diff --git a/docs.php b/docs.php index ebc3d873..bcc55fc1 100644 --- a/docs.php +++ b/docs.php @@ -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); diff --git a/src/Drupal/ContentTrait.php b/src/Drupal/ContentTrait.php index a58b12ca..fdbafe6f 100644 --- a/src/Drupal/ContentTrait.php +++ b/src/Drupal/ContentTrait.php @@ -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; @@ -24,6 +26,7 @@ */ trait ContentTrait { + use EntityFixtureTrait; use HelperTrait; /** @@ -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. * diff --git a/src/Drupal/EntityFixtureTrait.php b/src/Drupal/EntityFixtureTrait.php new file mode 100644 index 00000000..f7751cc4 --- /dev/null +++ b/src/Drupal/EntityFixtureTrait.php @@ -0,0 +1,126 @@ +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; + } + + if (is_array($value)) { + $value[0] = $fixture_path . $basename; + $stub->setValue($name, $value); + } + else { + $stub->setValue($name, $fixture_path . $basename); + } + } + } + + /** + * 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; + } + +} diff --git a/src/Drupal/MediaTrait.php b/src/Drupal/MediaTrait.php index 3d39297d..3c70850c 100644 --- a/src/Drupal/MediaTrait.php +++ b/src/Drupal/MediaTrait.php @@ -29,6 +29,7 @@ */ trait MediaTrait { + use EntityFixtureTrait; use HelperTrait; /** @@ -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); } /** diff --git a/tests/behat/bootstrap/FeatureContext.php b/tests/behat/bootstrap/FeatureContext.php index 0a1e9dee..4c7c0744 100644 --- a/tests/behat/bootstrap/FeatureContext.php +++ b/tests/behat/bootstrap/FeatureContext.php @@ -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; @@ -67,6 +68,7 @@ class FeatureContext extends DrupalContext { use EckTrait; use ElementTrait; use EmailTrait; + use EntityFixtureTrait; use FieldTrait; use FileDownloadTrait; use IframeTrait; diff --git a/tests/behat/features/drupal_content.feature b/tests/behat/features/drupal_content.feature index 2aaa8e95..efd82d32 100644 --- a/tests/behat/features/drupal_content.feature +++ b/tests/behat/features/drupal_content.feature @@ -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"