diff --git a/.ahoy.yml b/.ahoy.yml
index 59260df..4aa7311 100644
--- a/.ahoy.yml
+++ b/.ahoy.yml
@@ -110,19 +110,19 @@ commands:
lint:
usage: Lint code.
cmd: |
- ahoy cli "cp -Rf phpcs.xml phpstan.neon phpmd.xml gherkinlint.json build > /dev/null" \
+ ahoy cli "cp -Rf phpcs.xml phpstan.neon phpmd.xml gherkinlint.json rector.php build > /dev/null" \
&& ahoy cli "cd /app/build && ./vendor/bin/phpcs" \
+ && ahoy cli "cd /app/build && ./vendor/bin/phpstan" \
+ && ahoy cli "cd /app/build && ./vendor/bin/rector --clear-cache --dry-run" \
&& ahoy cli "cd /app/build && ./vendor/bin/phpmd ../src text phpmd.xml" \
&& ahoy cli "cd /app/build && ./vendor/bin/phpmd ../tests/behat/bootstrap text phpmd.xml" \
- && ahoy cli "cd /app/build && ./vendor/bin/phpstan" \
- && ahoy cli "cd /app/build && ./vendor/bin/rector process ../src --clear-cache --dry-run" \
&& ahoy cli "cd /app/build && ./vendor/bin/gherkinlint lint ../tests/behat/features"
lint-fix:
usage: Fix code.
cmd: |
- ahoy cli "cp -Rf phpcs.xml phpstan.neon build > /dev/null" \
- && ahoy cli "cd /app/build && ./vendor/bin/rector process ../src --clear-cache" \
+ ahoy cli "cp -Rf phpcs.xml phpstan.neon phpmd.xml gherkinlint.json rector.php build > /dev/null" \
+ && ahoy cli "cd /app/build && ./vendor/bin/rector --clear-cache" \
&& ahoy cli "cd /app/build && ./vendor/bin/phpcbf"
test-bdd:
diff --git a/composer.json b/composer.json
index bae2827..951d6dc 100644
--- a/composer.json
+++ b/composer.json
@@ -17,7 +17,7 @@
"source": "https://github.com/drevops/behat-steps"
},
"require": {
- "php": ">=8.1",
+ "php": ">=8.2",
"behat/behat": "^3",
"behat/mink": ">=1.11",
"behat/mink-selenium2-driver": ">=1.7",
diff --git a/phpcs.xml b/phpcs.xml
index 78d4fa4..6dd7213 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -4,6 +4,8 @@
+
+
@@ -11,6 +13,11 @@
+
+
+
+
+
../src
../tests/behat/bootstrap
diff --git a/phpstan.neon b/phpstan.neon
index e57d78b..25b174c 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -4,7 +4,9 @@
parameters:
- level: 5
+ level: 7
+
+ phpVersion: 80224
paths:
- ../src
@@ -13,6 +15,7 @@ parameters:
excludePaths:
- vendor/*
- node_modules/*
+ - ../tests/behat/bootstrap/BehatCliContext.php
scanFiles:
- vendor/behat/behat/bin/behat
diff --git a/tests/behat/fixtures/d10/rector.php b/rector.php
similarity index 82%
rename from tests/behat/fixtures/d10/rector.php
rename to rector.php
index 0e8db3c..f55c146 100644
--- a/tests/behat/fixtures/d10/rector.php
+++ b/rector.php
@@ -12,7 +12,7 @@
declare(strict_types=1);
-use DrupalFinder\DrupalFinder;
+use DrupalFinder\DrupalFinderComposerRuntime;
use DrupalRector\Set\Drupal10SetList;
use DrupalRector\Set\Drupal8SetList;
use DrupalRector\Set\Drupal9SetList;
@@ -23,11 +23,28 @@
use Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector;
+use Rector\Php80\Rector\Switch_\ChangeSwitchToMatchRector;
+use Rector\Php81\Rector\Array_\FirstClassCallableRector;
use Rector\Set\ValueObject\SetList;
use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector;
use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector;
return static function (RectorConfig $rectorConfig): void {
+ $drupalFinder = new DrupalFinderComposerRuntime();
+ $drupalRoot = $drupalFinder->getDrupalRoot();
+
+ $rectorConfig->autoloadPaths([
+ $drupalRoot . '/core',
+ $drupalRoot . '/modules',
+ $drupalRoot . '/themes',
+ $drupalRoot . '/profiles',
+ ]);
+
+ $rectorConfig->paths([
+ $drupalRoot . '/../../src',
+ $drupalRoot . '/../../tests/behat/bootstrap',
+ ]);
+
$rectorConfig->sets([
// Provided by Rector.
SetList::PHP_80,
@@ -46,21 +63,12 @@
$rectorConfig->rule(DeclareStrictTypesRector::class);
- $drupalFinder = new DrupalFinder();
- $drupalFinder->locateRoot(__DIR__);
-
- $drupalRoot = $drupalFinder->getDrupalRoot();
- $rectorConfig->autoloadPaths([
- $drupalRoot . '/core',
- $drupalRoot . '/modules',
- $drupalRoot . '/themes',
- $drupalRoot . '/profiles',
- ]);
-
$rectorConfig->skip([
// Rules added by Rector's rule sets.
+ ChangeSwitchToMatchRector::class,
CountArrayToEmptyArrayComparisonRector::class,
DisallowedEmptyRuleFixerRector::class,
+ FirstClassCallableRector::class,
InlineArrayReturnAssignRector::class,
NewlineAfterStatementRector::class,
NewlineBeforeNewAssignSetRector::class,
@@ -69,21 +77,7 @@
// Dependencies.
'*/vendor/*',
'*/node_modules/*',
- // Core and contribs.
- '*/core/*',
- '*/modules/contrib/*',
- '*/themes/contrib/*',
- '*/profiles/contrib/*',
- '*/sites/default/default.settings.php',
- // Files.
- '*/sites/default/files/*',
- '*/sites/simpletest/*',
- // Scaffold files.
- '*/autoload.php',
- '*/index.php',
- '*/update.php',
- // Composer scripts.
- '*/scripts/composer/*',
+ $drupalRoot . '/../../tests/behat/bootstrap/BehatCliContext.php',
]);
$rectorConfig->fileExtensions([
diff --git a/src/ContentTrait.php b/src/ContentTrait.php
index f974081..baa293e 100644
--- a/src/ContentTrait.php
+++ b/src/ContentTrait.php
@@ -41,7 +41,7 @@ public function contentRemoveContentType(string $type): void {
* @When I visit :type :title
*/
public function contentVisitPageWithTitle(string $type, string $title): void {
- $nids = $this->contentNodeLoadMultiple($type, [
+ $nids = $this->contentLoadMultiple($type, [
'title' => $title,
]);
@@ -67,7 +67,7 @@ public function contentVisitPageWithTitle(string $type, string $title): void {
* @When I edit :type :title
*/
public function contentEditPageWithTitle(string $type, string $title): void {
- $nids = $this->contentNodeLoadMultiple($type, [
+ $nids = $this->contentLoadMultiple($type, [
'title' => $title,
]);
@@ -87,7 +87,7 @@ public function contentEditPageWithTitle(string $type, string $title): void {
* @When I delete :type :title
*/
public function contentDeletePageWithTitle(string $type, string $title): void {
- $nids = $this->contentNodeLoadMultiple($type, [
+ $nids = $this->contentLoadMultiple($type, [
'title' => $title,
]);
@@ -115,7 +115,7 @@ public function contentDeletePageWithTitle(string $type, string $title): void {
*/
public function contentDelete(string $type, TableNode $nodesTable): void {
foreach ($nodesTable->getHash() as $nodeHash) {
- $nids = $this->contentNodeLoadMultiple($type, $nodeHash);
+ $nids = $this->contentLoadMultiple($type, $nodeHash);
$controller = \Drupal::entityTypeManager()->getStorage('node');
$entities = $controller->loadMultiple($nids);
@@ -161,7 +161,7 @@ public function contentModeratePageWithTitle(string $type, string $title, string
* @When I visit :type :title scheduled transitions
*/
public function contentVisitScheduledTransitionsPageWithTitle(string $type, string $title): void {
- $nids = $this->contentNodeLoadMultiple($type, [
+ $nids = $this->contentLoadMultiple($type, [
'title' => $title,
]);
@@ -180,13 +180,13 @@ public function contentVisitScheduledTransitionsPageWithTitle(string $type, stri
*
* @param string $type
* The node type.
- * @param array $conditions
+ * @param array $conditions
* Conditions keyed by field names.
*
- * @return array
+ * @return array
* Array of node ids.
*/
- protected function contentNodeLoadMultiple(string $type, array $conditions = []) {
+ protected function contentLoadMultiple(string $type, array $conditions = []): array {
$query = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', $type);
diff --git a/src/CookieTrait.php b/src/CookieTrait.php
index 92ce5d0..5bc6c7b 100644
--- a/src/CookieTrait.php
+++ b/src/CookieTrait.php
@@ -18,7 +18,7 @@ trait CookieTrait {
*
* @Then a cookie with( the) name :name should exist
*/
- public function cookieWithNameShouldExist($name): void {
+ public function cookieWithNameShouldExist(string $name): void {
static::cookieExists($name);
}
@@ -27,7 +27,7 @@ public function cookieWithNameShouldExist($name): void {
*
* @Then a cookie with( the) name :name and value :value should exist
*/
- public function cookieWithNameValueShouldExist($name, $value): void {
+ public function cookieWithNameValueShouldExist(string $name, string $value): void {
static::cookieExists($name, $value);
}
@@ -36,7 +36,7 @@ public function cookieWithNameValueShouldExist($name, $value): void {
*
* @Then a cookie with( the) name :name and value containing :partial_value should exist
*/
- public function cookieWithNamePartialValueShouldExist($name, $partial_value): void {
+ public function cookieWithNamePartialValueShouldExist(string $name, string $partial_value): void {
static::cookieExists($name, $partial_value, FALSE, TRUE);
}
@@ -45,7 +45,7 @@ public function cookieWithNamePartialValueShouldExist($name, $partial_value): vo
*
* @Then a cookie with( the) name containing :partial_name should exist
*/
- public function cookieWithPartialNameShouldExist($partial_name): void {
+ public function cookieWithPartialNameShouldExist(string $partial_name): void {
static::cookieExists($partial_name, NULL, TRUE);
}
@@ -54,7 +54,7 @@ public function cookieWithPartialNameShouldExist($partial_name): void {
*
* @Then a cookie with( the) name containing :partial_name and value :value should exist
*/
- public function cookieWithPartialNameValueShouldExist($partial_name, $value): void {
+ public function cookieWithPartialNameValueShouldExist(string $partial_name, string $value): void {
static::cookieExists($partial_name, $value, TRUE);
}
@@ -63,7 +63,7 @@ public function cookieWithPartialNameValueShouldExist($partial_name, $value): vo
*
* @Then a cookie with( the) name containing :partial_name and value containing :partial_value should exist
*/
- public function cookieWithPartialNamePartialValueShouldExist($partial_name, $partial_value): void {
+ public function cookieWithPartialNamePartialValueShouldExist(string $partial_name, string $partial_value): void {
static::cookieExists($partial_name, $partial_value, TRUE, TRUE);
}
@@ -72,7 +72,7 @@ public function cookieWithPartialNamePartialValueShouldExist($partial_name, $par
*
* @Then a cookie with( the) name :name should not exist
*/
- public function cookieWithNameShouldNotExist($name): void {
+ public function cookieWithNameShouldNotExist(string $name): void {
static::cookieNotExists($name);
}
@@ -81,7 +81,7 @@ public function cookieWithNameShouldNotExist($name): void {
*
* @Then a cookie with( the) name :name and value :value should not exist
*/
- public function cookieWithNameValueShouldNotExist($name, $value): void {
+ public function cookieWithNameValueShouldNotExist(string $name, string $value): void {
static::cookieNotExists($name, $value);
}
@@ -90,7 +90,7 @@ public function cookieWithNameValueShouldNotExist($name, $value): void {
*
* @Then a cookie with( the) name :name and value containing :partial_value should not exist
*/
- public function cookieWithNamePartialValueShouldNotExist($name, $partial_value): void {
+ public function cookieWithNamePartialValueShouldNotExist(string $name, string $partial_value): void {
static::cookieNotExists($name, $partial_value, FALSE, TRUE);
}
@@ -99,7 +99,7 @@ public function cookieWithNamePartialValueShouldNotExist($name, $partial_value):
*
* @Then a cookie with( the) name containing :partial_name should not exist
*/
- public function cookieWithPartialNameShouldNotExist($partial_name): void {
+ public function cookieWithPartialNameShouldNotExist(string $partial_name): void {
static::cookieNotExists($partial_name, NULL, TRUE);
}
@@ -108,7 +108,7 @@ public function cookieWithPartialNameShouldNotExist($partial_name): void {
*
* @Then a cookie with( the) name containing :partial_name and value :value should not exist
*/
- public function cookieWithPartialNameValueShouldNotExist($partial_name, $value): void {
+ public function cookieWithPartialNameValueShouldNotExist(string $partial_name, string $value): void {
static::cookieNotExists($partial_name, $value, TRUE);
}
@@ -117,14 +117,14 @@ public function cookieWithPartialNameValueShouldNotExist($partial_name, $value):
*
* @Then a cookie with( the) name containing :partial_name and value containing :partial_value should not exist
*/
- public function cookieWithPartialNamePartialValueShouldNotExist($partial_name, $partial_value): void {
+ public function cookieWithPartialNamePartialValueShouldNotExist(string $partial_name, string $partial_value): void {
static::cookieNotExists($partial_name, $partial_value, TRUE, TRUE);
}
/**
* Check if a cookie exists.
*/
- protected function cookieExists($name, $value = NULL, $is_partial_name = FALSE, $is_partial_value = FALSE): void {
+ protected function cookieExists(string $name, ?string $value = NULL, bool $is_partial_name = FALSE, bool $is_partial_value = FALSE): void {
$cookie = $this->cookieGetByName($name, $is_partial_name);
if ($cookie === NULL) {
@@ -137,7 +137,7 @@ protected function cookieExists($name, $value = NULL, $is_partial_name = FALSE,
if ($value !== NULL) {
if ($is_partial_value) {
- if (!str_contains((string) $cookie['value'], (string) $value)) {
+ if (!str_contains((string) $cookie['value'], $value)) {
if ($is_partial_name) {
throw new \Exception(sprintf('The cookie with name containing "%s" was set with value "%s", but it should contain "%s"', $name, $cookie['value'], $value));
}
@@ -156,7 +156,7 @@ protected function cookieExists($name, $value = NULL, $is_partial_name = FALSE,
/**
* Check if a cookie does not exist.
*/
- protected function cookieNotExists($name, $value = NULL, $is_partial_name = FALSE, $is_partial_value = FALSE): void {
+ protected function cookieNotExists(string $name, ?string $value = NULL, bool $is_partial_name = FALSE, bool $is_partial_value = FALSE): void {
$cookie = $this->cookieGetByName($name, $is_partial_name);
if ($cookie === NULL) {
@@ -165,7 +165,7 @@ protected function cookieNotExists($name, $value = NULL, $is_partial_name = FALS
if ($value !== NULL) {
if ($is_partial_value) {
- if (str_contains((string) $cookie['value'], (string) $value)) {
+ if (str_contains((string) $cookie['value'], $value)) {
if ($is_partial_name) {
throw new \Exception(sprintf('The cookie with name containing "%s" was set with value containing "%s", but it should not contain "%s"', $name, $cookie['value'], $value));
}
@@ -189,13 +189,21 @@ protected function cookieNotExists($name, $value = NULL, $is_partial_name = FALS
/**
* Get a cookie by exact or partial name.
+ *
+ * @param string $name
+ * The name of the cookie.
+ * @param bool $is_partial
+ * Whether to search for a partial name.
+ *
+ * @return array|null
+ * The cookie or NULL if not found.
*/
- protected function cookieGetByName($name, $is_partial = FALSE): ?array {
+ protected function cookieGetByName(string $name, bool $is_partial = FALSE): ?array {
$cookies = self::cookieGetAll();
foreach ($cookies as $cookie) {
if ($is_partial) {
- if (str_contains((string) $cookie['name'], (string) $name)) {
+ if (str_contains((string) $cookie['name'], $name)) {
return $cookie;
}
}
@@ -209,6 +217,9 @@ protected function cookieGetByName($name, $is_partial = FALSE): ?array {
/**
* Get all cookies.
+ *
+ * @return array>
+ * An array of cookies.
*/
protected function cookieGetAll(): array {
$driver = $this->getSession()->getDriver();
@@ -257,7 +268,10 @@ protected function cookieGetAll(): array {
continue;
}
- $cookies = self::cookieParseHeader($header_value);
+ // Only support parsed cookies from a string header.
+ if (is_string($header_value)) {
+ $cookies = self::cookieParseHeader($header_value);
+ }
break;
}
@@ -266,6 +280,12 @@ protected function cookieGetAll(): array {
/**
* Parse a cookie header string.
+ *
+ * @param string $string
+ * The cookie header string.
+ *
+ * @return array>
+ * An array of cookies.
*/
protected static function cookieParseHeader(string $string): array {
$cookies = [];
diff --git a/src/DateTrait.php b/src/DateTrait.php
index 37b7d34..d0727df 100644
--- a/src/DateTrait.php
+++ b/src/DateTrait.php
@@ -21,7 +21,7 @@ trait DateTrait {
* @Transform :value
* @Transform :expectedValue
*/
- public function dateRelativeTransformValue(string $value): string|array|null {
+ public function dateRelativeTransformValue(string $value): string {
return static::dateRelativeProcessValue($value);
}
@@ -79,7 +79,7 @@ protected static function dateRelativeStringHasToken(string $string): bool {
* To avoid this, default time is always rounded to midday and it is expected
* that relative time within a day use max of 12 hours offset.
*/
- public static function dateRelativeProcessValue(string $value, ?int $now = NULL): string|array|null {
+ public static function dateRelativeProcessValue(string $value, ?int $now = NULL): string {
// Inexpensive token detection and early exit.
if (!static::dateRelativeStringHasToken($value)) {
return $value;
@@ -88,12 +88,14 @@ public static function dateRelativeProcessValue(string $value, ?int $now = NULL)
// If `now` is not provided, round to the current hour to make sure that
// assertions are running within the same timeframe (for long tests).
$now = $now ?: strtotime(date('Y-m-d H:i:00', self::dateNow()));
+ $now = $now ?: NULL;
- return preg_replace_callback('/\[([relative:]+):([^]\[#]+)(?:#([^]\[]+))?]/', function (array $matches) use ($now) {
+ return (string) preg_replace_callback('/\[([relative:]+):([^]\[#]+)(?:#([^]\[]+))?]/', function (array $matches) use ($now): string {
$timestamp = strtotime($matches[2], $now);
if ($timestamp === FALSE) {
throw new \RuntimeException(sprintf('The supplied relative date cannot be evaluated: "%s"', $matches[1]));
}
+
// Convert to date format, if provided.
if (isset($matches[3])) {
$timestamp = date($matches[3], $timestamp);
@@ -103,14 +105,14 @@ public static function dateRelativeProcessValue(string $value, ?int $now = NULL)
throw new \RuntimeException(sprintf('The supplied relative date cannot be evaluated: "%s"', $matches[1]));
}
- return $timestamp;
+ return (string) $timestamp;
}, $value);
}
/**
* Get the current timestamp.
*/
- public static function dateNow(): int {
+ protected static function dateNow(): int {
return time();
}
diff --git a/src/DraggableViewsTrait.php b/src/DraggableViewsTrait.php
index d9e0e04..a0a42a9 100644
--- a/src/DraggableViewsTrait.php
+++ b/src/DraggableViewsTrait.php
@@ -64,8 +64,16 @@ public function draggableViewsSaveBundleOrder(string $view_id, string $view_disp
/**
* Find a node using provided conditions.
+ *
+ * @param string $type
+ * The node type.
+ * @param array $conditions
+ * The conditions to search for.
+ *
+ * @return \Drupal\node\NodeInterface|null
+ * The found node or NULL.
*/
- protected function draggableViewsFindNode(string $type, array $conditions): NodeInterface|NULL {
+ protected function draggableViewsFindNode(string $type, array $conditions): ?NodeInterface {
$query = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', $type);
diff --git a/src/EckTrait.php b/src/EckTrait.php
index 66ce29d..1787327 100644
--- a/src/EckTrait.php
+++ b/src/EckTrait.php
@@ -6,6 +6,7 @@
use Behat\Behat\Hook\Scope\AfterScenarioScope;
use Behat\Gherkin\Node\TableNode;
+use Drupal\eck\EckEntityInterface;
/**
* Trait EckTrait.
@@ -19,7 +20,7 @@ trait EckTrait {
/**
* Custom eck content entities organised by entity type.
*
- * @var array
+ * @var array>
*/
protected $eckEntities = [];
@@ -52,7 +53,7 @@ public function eckEntitiesCreate(string $bundle, string $entity_type, TableNode
*/
public function eckDeleteEntities(string $bundle, string $entity_type, TableNode $table): void {
foreach ($table->getHash() as $nodeHash) {
- $entity_ids = $this->eckEntityLoadMultiple($entity_type, $bundle, $nodeHash);
+ $entity_ids = $this->eckLoadMultiple($entity_type, $bundle, $nodeHash);
$controller = \Drupal::entityTypeManager()->getStorage($entity_type);
$entities = $controller->loadMultiple($entity_ids);
@@ -75,8 +76,9 @@ public function eckEntitiesCleanAll(AfterScenarioScope $scope): void {
$entity_ids_by_type = [];
foreach ($this->eckEntities as $entity_type => $content_entities) {
+ /** @var \Drupal\eck\EckEntityInterface $content_entity */
foreach ($content_entities as $content_entity) {
- $entity_ids_by_type[$entity_type][] = $content_entity->id;
+ $entity_ids_by_type[$entity_type][] = $content_entity->id();
}
}
@@ -96,13 +98,13 @@ public function eckEntitiesCleanAll(AfterScenarioScope $scope): void {
* The entity type.
* @param string $bundle
* The entity bundle.
- * @param array $conditions
+ * @param array $conditions
* Conditions keyed by field names.
*
- * @return array
+ * @return array
* Array of entity ids.
*/
- protected function eckEntityLoadMultiple(string $entity_type, string $bundle, array $conditions = []) {
+ protected function eckLoadMultiple(string $entity_type, string $bundle, array $conditions = []): array {
$query = \Drupal::entityQuery($entity_type)
->accessCheck(FALSE)
->condition('type', $bundle);
@@ -126,7 +128,7 @@ protected function eckEntityLoadMultiple(string $entity_type, string $bundle, ar
* @param \Behat\Gherkin\Node\TableNode $table
* The TableNode of entity data.
*/
- protected function eckCreateEntities(string $entity_type, string $bundle, TableNode $table) {
+ protected function eckCreateEntities(string $entity_type, string $bundle, TableNode $table): void {
foreach ($table->getHash() as $entity_hash) {
$entity = (object) $entity_hash;
$entity->type = $bundle;
@@ -140,7 +142,9 @@ protected function eckCreateEntities(string $entity_type, string $bundle, TableN
protected function eckCreateEntity(string $entity_type, \StdClass $entity): void {
$this->parseEntityFields($entity_type, $entity);
$saved = $this->getDriver()->createEntity($entity_type, $entity);
- $this->eckEntities[$entity_type][] = $saved;
+ if ($saved instanceof EckEntityInterface) {
+ $this->eckEntities[$entity_type][] = $saved;
+ }
}
/**
@@ -154,7 +158,7 @@ protected function eckCreateEntity(string $entity_type, \StdClass $entity): void
*/
public function eckEditEntityWithTitle(string $bundle, string $entity_type, string $label): void {
$entity_type_manager = \Drupal::entityTypeManager();
- $entity_ids = $this->eckEntityLoadMultiple($entity_type, $bundle, [
+ $entity_ids = $this->eckLoadMultiple($entity_type, $bundle, [
'title' => $label,
]);
@@ -181,7 +185,7 @@ public function eckEditEntityWithTitle(string $bundle, string $entity_type, stri
*/
public function eckVisitEntityPageWithTitle(string $bundle, string $entity_type, string $label): void {
$entity_type_manager = \Drupal::entityTypeManager();
- $entity_ids = $this->eckEntityLoadMultiple($entity_type, $bundle, [
+ $entity_ids = $this->eckLoadMultiple($entity_type, $bundle, [
'title' => $label,
]);
diff --git a/src/EmailTrait.php b/src/EmailTrait.php
index 4c86d74..6dcad65 100644
--- a/src/EmailTrait.php
+++ b/src/EmailTrait.php
@@ -8,6 +8,8 @@
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\PyStringNode;
use Drupal\Core\Database\Database;
+use Drupal\Core\Database\StatementInterface;
+use Drupal\user\UserInterface;
/**
* Trait EmailTrait.
@@ -21,7 +23,7 @@ trait EmailTrait {
/**
* List of email service types.
*
- * @var array
+ * @var array
*/
protected $emailTypes = [];
@@ -154,7 +156,7 @@ public function emailAssertNoEmailsWereSentToAddress(string $address): void {
*
* @Then an email header :header contains:
*/
- public function emailAssertEmailHeadersContains(string $header, PyStringNode $string, bool $exact = FALSE): array {
+ public function emailAssertEmailHeaderContains(string $header, PyStringNode $string, bool $exact = FALSE): void {
$string_value = (string) $string;
$string_value = $exact ? $string_value : trim((string) preg_replace('/\s+/', ' ', $string_value));
@@ -163,7 +165,7 @@ public function emailAssertEmailHeadersContains(string $header, PyStringNode $st
$header_value = $exact ? $header_value : trim((string) preg_replace('/\s+/', ' ', (string) $header_value));
if (str_contains((string) $header_value, $string_value)) {
- return $record;
+ return;
}
}
@@ -175,8 +177,8 @@ public function emailAssertEmailHeadersContains(string $header, PyStringNode $st
*
* @Then an email header :header contains exact:
*/
- public function emailAssertEmailHeadersContainsExact(string $header, PyStringNode $string): void {
- $this->emailAssertEmailHeadersContains($header, $string, TRUE);
+ public function emailAssertEmailHeaderContainsExact(string $header, PyStringNode $string): void {
+ $this->emailAssertEmailHeaderContains($header, $string, TRUE);
}
/**
@@ -186,12 +188,13 @@ public function emailAssertEmailHeadersContainsExact(string $header, PyStringNod
*/
public function emailAssertEmailToUserIsActionWithContent(string $name, string $action, string $field, PyStringNode $string): void {
$user = $name === 'current' && !empty($this->getUserManager()->getCurrentUser()) ? $this->getUserManager()->getCurrentUser() : user_load_by_name($name);
- if (!$user) {
+
+ if (!$user instanceof UserInterface) {
throw new \Exception(sprintf('Unable to find a user "%s"', $name));
}
if ($action === 'sent') {
- $this->emailAssertEmailContains('to', new PyStringNode([$user->mail], 0), TRUE);
+ $this->emailAssertEmailContains('to', new PyStringNode([$user->getEmail()], 0), TRUE);
$this->emailAssertEmailContains($field, $string);
}
elseif ($action === 'not sent') {
@@ -208,24 +211,12 @@ public function emailAssertEmailToUserIsActionWithContent(string $name, string $
* @Then an email :field contains
* @Then an email :field contains:
*/
- public function emailAssertEmailContains(string $field, PyStringNode $string, bool $exact = FALSE): array {
- if (!in_array($field, ['subject', 'body', 'to', 'from'])) {
- throw new \RuntimeException(sprintf('Invalid email field %s was specified for assertion', $field));
- }
-
- $string = strval($string);
- $string = $exact ? $string : trim((string) preg_replace('/\s+/', ' ', $string));
+ public function emailAssertEmailContains(string $field, PyStringNode $string, bool $exact = FALSE): void {
+ $email = $this->emailFind($field, $string, $exact);
- foreach (self::emailGetCollectedEmails() as $record) {
- $field_string = $record[$field] ?? '';
- $field_string = $exact ? $field_string : trim((string) preg_replace('/\s+/', ' ', (string) $field_string));
-
- if (str_contains((string) $field_string, $string)) {
- return $record;
- }
+ if (!$email) {
+ throw new \Exception(sprintf('Unable to find email with%s text "%s" in field "%s" retrieved from test email collector.', ($exact ? ' exact' : ''), $string, $field));
}
-
- throw new \Exception(sprintf('Unable to find email with%s text "%s" in field "%s" retrieved from test email collector.', ($exact ? ' exact' : ''), $string, $field));
}
/**
@@ -280,8 +271,23 @@ public function emailAssertEmailNotContainsExact(string $field, PyStringNode $st
public function emailFollowLinkNumber(string $number, PyStringNode $subject): void {
$number = intval($number);
- $email = $this->emailAssertEmailContains('subject', $subject);
- $links = self::emailExtractLinks($email['params']['body'] ?? $email['body'] ?? '');
+ $email = $this->emailFind('subject', $subject);
+
+ if (!$email) {
+ throw new \Exception(sprintf('Unable to find email with subject "%s" retrieved from test email collector.', $subject));
+ }
+
+ if (isset($email['params']['body']) && is_string($email['params']['body'])) {
+ $body = $email['params']['body'];
+ }
+ elseif (is_string($email['body'])) {
+ $body = $email['body'];
+ }
+ else {
+ throw new \Exception('No body found in email');
+ }
+
+ $links = self::emailExtractLinks($body);
if (empty($links)) {
throw new \Exception(sprintf('No links were found in the email with subject %s', $subject));
@@ -303,11 +309,17 @@ public function emailFollowLinkNumber(string $number, PyStringNode $subject): vo
* @Then file :name attached to the email with the subject:
*/
public function emailAssertEmailContainsAttachmentWithName(string $name, PyStringNode $subject): void {
- $email = $this->emailAssertEmailContains('subject', $subject);
+ $email = $this->emailFind('subject', $subject);
- foreach ($email['params']['attachments'] as $attachment) {
- if ($attachment['filename'] == $name) {
- return;
+ if (!$email) {
+ throw new \Exception(sprintf('Unable to find email with subject "%s" retrieved from test email collector.', $subject));
+ }
+
+ if (!empty($email['params']['attachments'])) {
+ foreach ($email['params']['attachments'] as $attachment) {
+ if ($attachment['filename'] == $name) {
+ return;
+ }
}
}
@@ -397,11 +409,18 @@ protected static function emailDeleteMailSystemOriginal(): void {
/**
* Get emails collected during the test.
+ *
+ * @return array>
+ * Array of collected emails.
*/
protected function emailGetCollectedEmails(): array {
// Directly read data from the database to avoid cache invalidation that
// may corrupt the system under test.
- $emails = array_map(unserialize(...), Database::getConnection()->query("SELECT name, value FROM {key_value} WHERE name = 'system.test_mail_collector'")->fetchAllKeyed());
+ $query = Database::getConnection()->query("SELECT name, value FROM {key_value} WHERE name = 'system.test_mail_collector'");
+
+ if ($query instanceof StatementInterface) {
+ $emails = array_map(unserialize(...), $query->fetchAllKeyed());
+ }
$emails = empty($emails['system.test_mail_collector']) ? [] : $emails['system.test_mail_collector'];
@@ -422,13 +441,46 @@ protected function emailGetCollectedEmails(): array {
return $emails;
}
+ /**
+ * Find an email message field containing a value.
+ *
+ * @param string $field
+ * Field to search in.
+ * @param \Behat\Gherkin\Node\PyStringNode $string
+ * String to search for.
+ * @param bool $exact
+ * Whether to search for an exact match.
+ *
+ * @return array>|null
+ * Email record or NULL if not found.
+ */
+ protected function emailFind(string $field, PyStringNode $string, bool $exact = FALSE): ?array {
+ if (!in_array($field, ['subject', 'body', 'to', 'from'])) {
+ throw new \RuntimeException(sprintf('Invalid email field %s was specified for assertion', $field));
+ }
+
+ $string = strval($string);
+ $string = $exact ? $string : trim((string) preg_replace('/\s+/', ' ', $string));
+
+ foreach (self::emailGetCollectedEmails() as $record) {
+ $field_string = $record[$field] ?? '';
+ $field_string = $exact ? $field_string : trim((string) preg_replace('/\s+/', ' ', (string) $field_string));
+
+ if (str_contains((string) $field_string, $string)) {
+ return $record;
+ }
+ }
+
+ return NULL;
+ }
+
/**
* Extract all links from provided string.
*
* @param string $string
* String to extract links from.
*
- * @return array
+ * @return array
* Array of extracted links.
*/
protected static function emailExtractLinks(string $string): array {
@@ -445,6 +497,12 @@ protected static function emailExtractLinks(string $string): array {
/**
* Extract email types from tags.
+ *
+ * @param array $tags
+ * Array of tags.
+ *
+ * @return array
+ * Array of email types.
*/
protected static function emailExtractTypes(array $tags): array {
$types = [];
diff --git a/src/FileDownloadTrait.php b/src/FileDownloadTrait.php
index 66a84a6..1bec42c 100644
--- a/src/FileDownloadTrait.php
+++ b/src/FileDownloadTrait.php
@@ -24,7 +24,7 @@ trait FileDownloadTrait {
/**
* Information about downloaded file.
*
- * @var array
+ * @var array
*/
protected $fileDownloadDownloadedFileInfo;
@@ -147,16 +147,20 @@ public function fileDownloadAssertFileContains(PyStringNode $string): void {
if (!$this->fileDownloadDownloadedFileInfo) {
throw new \RuntimeException('Downloaded file content has no data.');
}
+
$lines = preg_split('/\R/', (string) $this->fileDownloadDownloadedFileInfo['content']);
- foreach ($lines as $line) {
- if (preg_match('/^\/.+\/[a-z]*$/i', $string)) {
- if (preg_match($string, $line)) {
+
+ if (is_array($lines)) {
+ foreach ($lines as $line) {
+ if (preg_match('/^\/.+\/[a-z]*$/i', $string)) {
+ if (preg_match($string, $line)) {
+ return;
+ }
+ }
+ elseif (str_contains($line, $string)) {
return;
}
}
- elseif (str_contains($line, $string)) {
- return;
- }
}
throw new \Exception('Unable to find a content line with searched string.');
@@ -255,6 +259,14 @@ protected function fileDownloadOpenZip(): \ZipArchive {
/**
* Download file.
+ *
+ * @param string $url
+ * URL to download file from.
+ * @param array $options
+ * CURL options.
+ *
+ * @return array
+ * Array of downloaded file information.
*/
protected function fileDownloadProcess(string $url, array $options = []): array {
$response_headers = [];
@@ -295,11 +307,17 @@ protected function fileDownloadProcess(string $url, array $options = []): array
// Resolve file path and name.
$dir = $this->fileDownloadGetTempDir();
+
// Try to extract name from the download string.
$url_file_name = parse_url($url, PHP_URL_PATH);
$url_file_name = $url_file_name ? basename($url_file_name) : $url_file_name;
$headers['file_name'] = empty($headers['file_name']) && !empty($url_file_name) ? $url_file_name : $headers['file_name'];
+
$file_path = empty($headers['file_name']) ? tempnam($dir, 'behat') : $dir . DIRECTORY_SEPARATOR . $headers['file_name'];
+ if (!$file_path) {
+ throw new \RuntimeException('Unable to create temp file for downloaded content');
+ }
+
$file_name = basename($file_path);
// Write file contents.
@@ -316,14 +334,15 @@ protected function fileDownloadProcess(string $url, array $options = []): array
/**
* Extract downloaded file information from the response headers.
*
- * @param array $headers
+ * @param array $headers
* Array of headers from CURL.
*
- * @return array
+ * @return array
* Array of parsed headers, if any.
*/
protected function fileDownloadParseHeaders(array $headers): array {
$parsed_headers = [];
+
foreach ($headers as $header) {
if (preg_match('/Content-Disposition:\s*attachment;\s*filename\s*=\s*\"([^"]+)"/', (string) $header, $matches) && !empty($matches[1])) {
$parsed_headers['file_name'] = trim($matches[1]);
@@ -332,7 +351,6 @@ protected function fileDownloadParseHeaders(array $headers): array {
if (preg_match('/Content-Type:\s*(.+)/', (string) $header, $matches) && !empty($matches[1])) {
$parsed_headers['content_type'] = trim($matches[1]);
- continue;
}
}
diff --git a/src/FileTrait.php b/src/FileTrait.php
index 3b3c0be..f2590c5 100644
--- a/src/FileTrait.php
+++ b/src/FileTrait.php
@@ -22,16 +22,16 @@
trait FileTrait {
/**
- * Files ids.
+ * Files entities.
*
- * @var array
+ * @var array
*/
- protected $files = [];
+ protected $fileEntities = [];
/**
* Unmanaged file URIs.
*
- * @var array
+ * @var array
*/
protected $filesUnmanagedUris = [];
@@ -77,23 +77,34 @@ public function fileCreateManaged(TableNode $nodesTable): void {
protected function fileCreateManagedSingle(\StdClass $stub): FileInterface {
$this->parseEntityFields('file', $stub);
$saved = $this->fileCreateEntity($stub);
- $this->files[] = $saved;
+
+ $this->fileEntities[] = $saved;
return $saved;
}
/**
* Create file entity.
+ *
+ * @param \StdClass $stub
+ * Stub object.
+ *
+ * @return \Drupal\file\FileInterface
+ * Created file entity.
+ *
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
protected function fileCreateEntity(\StdClass $stub): FileInterface {
if (empty($stub->path)) {
throw new \RuntimeException('"path" property is required');
}
+
$path = ltrim($stub->path, '/');
// Get fixture file path.
- if ($this->getMinkParameter('files_path')) {
- $full_path = rtrim(realpath($this->getMinkParameter('files_path')), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $path;
+ if (!empty($this->getMinkParameter('files_path'))) {
+ $full_path = rtrim((string) realpath($this->getMinkParameter('files_path')), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $path;
if (is_file($full_path)) {
$path = $full_path;
}
@@ -113,7 +124,12 @@ protected function fileCreateEntity(\StdClass $stub): FileInterface {
}
}
- $entity = \Drupal::service('file.repository')->writeData(file_get_contents($path), $destination, FileExists::Replace);
+ $content = file_get_contents($path);
+ if ($content === FALSE) {
+ throw new \RuntimeException('Unable to read file ' . $path);
+ }
+
+ $entity = \Drupal::service('file.repository')->writeData($content, $destination, FileExists::Replace);
$fields = get_object_vars($stub);
foreach ($fields as $property => $value) {
@@ -141,7 +157,7 @@ public function fileCleanAll(AfterScenarioScope $scope): void {
return;
}
- foreach ($this->files as $file) {
+ foreach ($this->fileEntities as $file) {
$file->delete();
}
@@ -149,7 +165,7 @@ public function fileCleanAll(AfterScenarioScope $scope): void {
@unlink($uri);
}
- $this->files = [];
+ $this->fileEntities = [];
}
/**
@@ -177,11 +193,19 @@ public function fileCleanAll(AfterScenarioScope $scope): void {
*/
public function fileDeleteManagedFiles(TableNode $nodesTable): void {
$storage = \Drupal::entityTypeManager()->getStorage('file');
+
$field_values = $nodesTable->getColumn(0);
// Get field name of the column header.
$field_name = array_shift($field_values);
+
+ if (is_numeric($field_name)) {
+ throw new \RuntimeException('The first column should be the field name');
+ }
+
+ $field_name = (string) $field_name;
+
foreach ($field_values as $field_value) {
- $ids = $this->fileLoadMultiple([$field_name => $field_value]);
+ $ids = $this->fileLoadMultiple([$field_name => (string) $field_value]);
$entities = $storage->loadMultiple($ids);
$storage->delete($entities);
}
@@ -190,14 +214,15 @@ public function fileDeleteManagedFiles(TableNode $nodesTable): void {
/**
* Load multiple files with specified conditions.
*
- * @param array $conditions
+ * @param array $conditions
* Conditions keyed by field names.
*
- * @return array
+ * @return array
* Array of file ids.
*/
- protected function fileLoadMultiple(array $conditions = []): array|int {
+ protected function fileLoadMultiple(array $conditions = []): array {
$query = \Drupal::entityQuery('file')->accessCheck(FALSE);
+
foreach ($conditions as $k => $v) {
$and = $query->andConditionGroup();
$and->condition($k, $v);
@@ -267,6 +292,9 @@ public function fileAssertUnmanagedHasContent(string $uri, string $content): voi
$this->fileAssertUnmanagedExists($uri);
$file_content = @file_get_contents($uri);
+ if ($file_content === FALSE) {
+ throw new \Exception(sprintf('Unable to read file %s.', $uri));
+ }
if (!str_contains($file_content, $content)) {
throw new \Exception(sprintf('File contents "%s" does not contain "%s".', $file_content, $content));
@@ -282,6 +310,9 @@ public function fileAssertUnmanagedHasNoContent(string $uri, string $content): v
$this->fileAssertUnmanagedExists($uri);
$file_content = @file_get_contents($uri);
+ if ($file_content === FALSE) {
+ throw new \Exception(sprintf('Unable to read file %s.', $uri));
+ }
if (str_contains($file_content, $content)) {
throw new \Exception(sprintf('File contents "%s" contains "%s", but should not.', $file_content, $content));
diff --git a/src/KeyboardTrait.php b/src/KeyboardTrait.php
index 3715a54..6e4f253 100644
--- a/src/KeyboardTrait.php
+++ b/src/KeyboardTrait.php
@@ -140,7 +140,7 @@ public function keyboardPressKeyOnElement(string $char, ?string $selector = NULL
* @throws \Behat\Mink\Exception\UnsupportedDriverActionException
* If method is used for invalid driver.
*/
- protected function keyboardTriggerKey(string $xpath, string $key) {
+ protected function keyboardTriggerKey(string $xpath, string $key): void {
$driver = $this->getSession()->getDriver();
if (!$driver instanceof Selenium2Driver) {
throw new UnsupportedDriverActionException('Method can be used only with Selenium2 driver', $driver);
diff --git a/src/LinkTrait.php b/src/LinkTrait.php
index d76baeb..71aaaf6 100644
--- a/src/LinkTrait.php
+++ b/src/LinkTrait.php
@@ -182,7 +182,7 @@ public function assertLinkNotAbsolute(string $text): void {
/**
* Returns fixed step argument (with \\" replaced back to ").
*/
- protected function linkFixStepArgument(string $argument): string|array {
+ protected function linkFixStepArgument(string $argument): string {
return str_replace('\\"', '"', $argument);
}
diff --git a/src/MediaTrait.php b/src/MediaTrait.php
index d18d915..d73424b 100644
--- a/src/MediaTrait.php
+++ b/src/MediaTrait.php
@@ -7,6 +7,7 @@
use Behat\Behat\Hook\Scope\AfterScenarioScope;
use Behat\Gherkin\Node\TableNode;
use Drupal\media\Entity\Media;
+use Drupal\media\MediaInterface;
/**
* Trait MediaTrait.
@@ -21,9 +22,9 @@ trait MediaTrait {
/**
* Array of created media entities.
*
- * @var array
+ * @var array
*/
- protected $media = [];
+ protected $mediaEntities = [];
/**
* Remove any created media items.
@@ -36,10 +37,10 @@ public function mediaClean(AfterScenarioScope $scope): void {
return;
}
- foreach ($this->media as $media) {
+ foreach ($this->mediaEntities as $media) {
$media->delete();
}
- $this->media = [];
+ $this->mediaEntities = [];
}
/**
@@ -90,31 +91,66 @@ public function mediaCreate(string $type, TableNode $nodesTable): void {
*
* @Given /^no ([a-zA-z0-9_-]+) media:$/
*/
- public function mediaDelete(string $type, TableNode $nodesTable): void {
- foreach ($nodesTable->getHash() as $nodeHash) {
- $ids = $this->mediaLoadMultiple($type, $nodeHash);
-
+ public function mediaDelete(string $type, TableNode $table): void {
+ foreach ($table->getHash() as $node_hash) {
+ $ids = $this->mediaLoadMultiple($type, $node_hash);
$controller = \Drupal::entityTypeManager()->getStorage('media');
$entities = $controller->loadMultiple($ids);
$controller->delete($entities);
}
}
+ /**
+ * Navigate to edit media with specified type and name.
+ *
+ * @code
+ * When I edit "document" media "Test document"
+ * @endcode
+ *
+ * @When I edit :type media :name
+ */
+ public function mediaEditWithName(string $type, string $name): void {
+ $mids = $this->mediaLoadMultiple($type, [
+ 'name' => $name,
+ ]);
+
+ if (empty($mids)) {
+ throw new \RuntimeException(sprintf('Unable to find %s media "%s"', $type, $name));
+ }
+
+ $mid = current($mids);
+ $path = $this->locatePath('/media/' . $mid) . '/edit';
+ print $path;
+ $this->getSession()->visit($path);
+ }
+
/**
* Create a single media item.
+ *
+ * @param \StdClass $stub
+ * The media item properties.
+ *
+ * @return \Drupal\media\MediaInterface
+ * The created media item.
*/
- protected function mediaCreateSingle(\StdClass $stub) {
+ protected function mediaCreateSingle(\StdClass $stub): MediaInterface {
$this->parseEntityFields('media', $stub);
$saved = $this->mediaCreateEntity($stub);
- $this->media[] = $saved;
+ $this->mediaEntities[] = $saved;
return $saved;
}
/**
* Create media entity.
+ *
+ * @param \StdClass $stub
+ * The media entity properties.
+ *
+ * @return \Drupal\media\MediaInterface
+ * The created media entity.
*/
- protected function mediaCreateEntity(\StdClass $stub) {
+ protected function mediaCreateEntity(\StdClass $stub): MediaInterface {
// Throw an exception if the media type is missing or does not exist.
if (!property_exists($stub, 'bundle') || $stub->bundle === NULL || !$stub->bundle) {
throw new \Exception("Cannot create media because it is missing the required property 'bundle'.");
@@ -139,6 +175,11 @@ protected function mediaCreateEntity(\StdClass $stub) {
* Expand parsed fields into expected field values based on field type.
*
* This is a re-use of the functionality provided by DrupalExtension.
+ *
+ * @param string $entity_type
+ * The entity type.
+ * @param \StdClass $stub
+ * The entity stub.
*/
protected function mediaExpandEntityFields(string $entity_type, \StdClass $stub): void {
$core = $this->getDriver()->getCore();
@@ -152,9 +193,18 @@ protected function mediaExpandEntityFields(string $entity_type, \StdClass $stub)
/**
* Expand entity fields with fixture values.
+ *
+ * @param \StdClass $stub
+ * The entity stub.
*/
- protected function mediaExpandEntityFieldsFixtures(\StdClass $stub) {
- $fixture_path = $this->getMinkParameter('files_path') ? rtrim(realpath($this->getMinkParameter('files_path')), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : NULL;
+ protected function mediaExpandEntityFieldsFixtures(\StdClass $stub): void {
+ if (!empty($this->getMinkParameter('files_path'))) {
+ $fixture_path = rtrim((string) realpath($this->getMinkParameter('files_path')), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+ }
+
+ 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.');
+ }
$fields = get_object_vars($stub);
@@ -178,42 +228,18 @@ protected function mediaExpandEntityFieldsFixtures(\StdClass $stub) {
}
}
- /**
- * Navigate to edit media with specified type and name.
- *
- * @code
- * When I edit "document" media "Test document"
- * @endcode
- *
- * @When I edit :type media :name
- */
- public function mediaEditWithName(string $type, string $name): void {
- $mids = $this->mediaLoadMultiple($type, [
- 'name' => $name,
- ]);
-
- if (empty($mids)) {
- throw new \RuntimeException(sprintf('Unable to find %s media "%s"', $type, $name));
- }
-
- $mid = current($mids);
- $path = $this->locatePath('/media/' . $mid) . '/edit';
- print $path;
- $this->getSession()->visit($path);
- }
-
/**
* Load multiple media entities with specified type and conditions.
*
* @param string $type
* The node type.
- * @param array $conditions
+ * @param array $conditions
* Conditions keyed by field names.
*
- * @return array
+ * @return array
* Array of node ids.
*/
- protected function mediaLoadMultiple(string $type, array $conditions = []) {
+ protected function mediaLoadMultiple(string $type, array $conditions = []): array {
$query = \Drupal::entityQuery('media')
->accessCheck(FALSE)
->condition('bundle', $type);
diff --git a/src/MenuTrait.php b/src/MenuTrait.php
index d318b57..9694367 100644
--- a/src/MenuTrait.php
+++ b/src/MenuTrait.php
@@ -8,6 +8,7 @@
use Behat\Gherkin\Node\TableNode;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\system\Entity\Menu;
+use Drupal\system\MenuInterface;
/**
* Trait MenuTrait.
@@ -43,12 +44,10 @@ trait MenuTrait {
*/
public function menuDelete(TableNode $table): void {
foreach ($table->getColumn(0) as $label) {
- try {
- $menu = $this->loadMenuByLabel($label);
+ $menu = $this->loadMenuByLabel($label);
+ if ($menu instanceof MenuInterface) {
$menu->delete();
}
- catch (\Exception) {
- }
}
}
@@ -66,14 +65,16 @@ public function menuDelete(TableNode $table): void {
public function menuCreate(TableNode $table): void {
foreach ($table->getHash() as $menu_hash) {
if (empty($menu_hash['id'])) {
- // Create menu id if one not provided.
+ // Create menu id if one was not provided.
$menu_id = strtolower((string) $menu_hash['label']);
$menu_id = preg_replace('/[^a-z0-9_]+/', '_', $menu_id);
$menu_id = preg_replace('/_+/', '_', (string) $menu_id);
$menu_hash['id'] = $menu_id;
}
+
$menu = Menu::create($menu_hash);
$menu->save();
+
$this->menus[] = $menu;
}
}
@@ -89,12 +90,9 @@ public function menuCreate(TableNode $table): void {
*/
public function menuLinksDelete(string $menu_name, TableNode $table): void {
foreach ($table->getColumn(0) as $title) {
- try {
- $menu_link = $this->loadMenuLinkByTitle($title, $menu_name);
- $menu_link?->delete();
- }
- catch (\Exception) {
- continue;
+ $menu_link = $this->loadMenuLinkByTitle($title, $menu_name);
+ if ($menu_link instanceof MenuLinkContent) {
+ $menu_link->delete();
}
}
}
@@ -113,6 +111,11 @@ public function menuLinksDelete(string $menu_name, TableNode $table): void {
*/
public function menuLinksCreate(string $menu_name, TableNode $table): void {
$menu = $this->loadMenuByLabel($menu_name);
+
+ if (!$menu instanceof MenuInterface) {
+ throw new \RuntimeException(sprintf('Menu "%s" not found', $menu_name));
+ }
+
foreach ($table->getHash() as $menu_link_hash) {
$menu_link_hash['menu_name'] = $menu->id();
// Add uri to correct property.
@@ -142,65 +145,77 @@ public function menuCleanAll(AfterScenarioScope $scope): void {
if ($scope->getScenario()->hasTag('behat-steps-skip:' . __FUNCTION__)) {
return;
}
- // Clean up created menus.
- foreach ($this->menus as $menu) {
- try {
- $menu->delete();
- }
- catch (\Exception) {
- // Ignore the exception and move on.
- continue;
- }
- }
- // Clean up menu links.
+
foreach ($this->menuLinks as $menu_link) {
- try {
- $menu_link->delete();
- }
- catch (\Exception) {
- // Ignore the exception and move on.
- continue;
- }
+ $menu_link->delete();
}
-
$this->menuLinks = [];
+ foreach ($this->menus as $menu) {
+ $menu->delete();
+ }
$this->menus = [];
}
/**
* Gets a menu by label.
+ *
+ * @param string $label
+ * The label of the menu.
+ *
+ * @return \Drupal\system\MenuInterface|null
+ * The menu or NULL if not found.
*/
- protected function loadMenuByLabel(string $label) {
+ protected function loadMenuByLabel(string $label): ?MenuInterface {
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = \Drupal::getContainer()->get('entity_type.manager');
- $query = $entity_type_manager->getStorage('menu')->getQuery();
- $query->accessCheck(FALSE);
- $query->condition('label', $label);
- $menu_ids = $query->execute();
- $menu_id = reset($menu_ids);
- if ($menu_id === FALSE) {
- throw new \Exception(sprintf('Could not find the %s menu.', $label));
+ $menu_ids = $entity_type_manager->getStorage('menu')->getQuery()
+ ->accessCheck(FALSE)
+ ->condition('label', $label)
+ ->execute();
+
+ if (empty($menu_ids)) {
+ return NULL;
}
+ $menu_id = reset($menu_ids);
+
return Menu::load($menu_id);
}
/**
* Gets a menu link by title and menu name.
+ *
+ * @param string $title
+ * The title of the menu link.
+ * @param string $menu_name
+ * The name of the menu.
+ *
+ * @return \Drupal\menu_link_content\Entity\MenuLinkContent|null
+ * The menu link or NULL if not found.
*/
- protected function loadMenuLinkByTitle(string $title, string $menu_name) {
+ protected function loadMenuLinkByTitle(string $title, string $menu_name): ?MenuLinkContent {
$menu = $this->loadMenuByLabel($menu_name);
+
+ if (!$menu instanceof MenuInterface) {
+ return NULL;
+ }
+
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = \Drupal::getContainer()->get('entity_type.manager');
- $query = $entity_type_manager->getStorage('menu_link_content')->getQuery();
- $query->accessCheck(FALSE);
- $menu_link_ids = $query->condition('menu_name', $menu->id())->condition('title', $title)->execute();
- $menu_link_id = reset($menu_link_ids);
- if ($menu_link_id === FALSE) {
- throw new \Exception(sprintf('Could not find the %s menu link in %s menu.', $title, $menu_name));
+
+ $menu_link_ids = $entity_type_manager->getStorage('menu_link_content')->getQuery()
+ ->accessCheck(FALSE)
+ ->condition('menu_name', $menu->id())
+ ->condition('title', $title)
+ ->execute();
+
+ if (empty($menu_link_ids)) {
+ return NULL;
}
+ $menu_link_id = reset($menu_link_ids);
+
return MenuLinkContent::load($menu_link_id);
}
diff --git a/src/ParagraphsTrait.php b/src/ParagraphsTrait.php
index 3323c72..bb8ad6c 100644
--- a/src/ParagraphsTrait.php
+++ b/src/ParagraphsTrait.php
@@ -8,6 +8,7 @@
use Behat\Gherkin\Node\TableNode;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\paragraphs\Entity\Paragraph;
+use Drupal\paragraphs\ParagraphInterface;
/**
* Trait ParagraphsTrait.
@@ -23,9 +24,9 @@ trait ParagraphsTrait {
/**
* Array of created paragraph entities.
*
- * @var array
+ * @var array
*/
- protected static $paragraphs = [];
+ protected static $paragraphEntities = [];
/**
* Clean all paragraphs instances after scenario run.
@@ -38,16 +39,10 @@ public function paragraphsCleanAll(AfterScenarioScope $scope): void {
return;
}
- foreach (static::$paragraphs as $paragraph) {
- try {
- $paragraph->delete();
- }
- catch (\Exception) {
- // Ignore the exception and move on.
- continue;
- }
+ foreach (static::$paragraphEntities as $paragraph) {
+ $paragraph->delete();
}
- static::$paragraphs = [];
+ static::$paragraphEntities = [];
}
/**
@@ -66,12 +61,11 @@ public function paragraphsAddToEntityWithFields(string $field_name, string $bund
// Find previously created entity by entity_type, bundle and identifying
// field value.
- $entity = $this->paragraphsFindEntity([
- 'field_value' => $entity_field_identifer,
- 'field_name' => $entity_field_name,
- 'bundle' => $bundle,
- 'entity_type' => $entity_type,
- ]);
+ $parent_entity = $this->paragraphsFindEntity($entity_type, $bundle, $entity_field_name, $entity_field_identifer);
+
+ if (!$parent_entity) {
+ throw new \RuntimeException(sprintf('Parent entity "%s" with field "%s" of value "%s" not found', $bundle, $entity_field_name, $entity_field_identifer));
+ }
// Get fields from scenario, parse them and expand values according to
// field tables.
@@ -80,16 +74,15 @@ public function paragraphsAddToEntityWithFields(string $field_name, string $bund
$this->parseEntityFields('paragraph', $stub);
$this->paragraphsExpandEntityFields('paragraph', $stub);
- // Attach paragraph from stub to node.
- $this->paragraphsAttachFromStubToEntity($entity, $field_name, $paragraph_type, $stub);
+ $this->paragraphsAttachFromStubToEntity($parent_entity, $field_name, $paragraph_type, $stub);
}
/**
* Create a paragraphs item from a stub and attach it to an entity.
*
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+ * @param \Drupal\Core\Entity\ContentEntityInterface $parent_entity
* Node to attach paragraph to.
- * @param string $entity_field_name
+ * @param string $parent_entity_field_name
* Field name on the entity that refers paragraphs item.
* @param string $paragraph_bundle
* Paragraphs item bundle name.
@@ -99,60 +92,73 @@ public function paragraphsAddToEntityWithFields(string $field_name, string $bund
* @param bool $save_entity
* Flag to save node after attaching a paragraphs item. Defaults to TRUE.
*
- * @return \Drupal\paragraphs\Entity\Paragraph
+ * @return \Drupal\paragraphs\ParagraphInterface
* Created paragraphs item.
*/
- protected function paragraphsAttachFromStubToEntity(ContentEntityInterface $entity, string $entity_field_name, string $paragraph_bundle, \StdClass $stub, bool $save_entity = TRUE): Paragraph {
+ protected function paragraphsAttachFromStubToEntity(ContentEntityInterface $parent_entity, string $parent_entity_field_name, string $paragraph_bundle, \StdClass $stub, bool $save_entity = TRUE): ParagraphInterface {
$stub->type = $paragraph_bundle;
$stub = (array) $stub;
+
$paragraph = Paragraph::create($stub);
- $paragraph->setParentEntity($entity, $entity_field_name)->save();
- $existing_value = $entity->get($entity_field_name);
- $new_value = $existing_value->getValue();
+ $paragraph->setParentEntity($parent_entity, $parent_entity_field_name)->save();
+
+ $new_value = $parent_entity->get($parent_entity_field_name)->getValue();
$new_value[] = [
'target_id' => $paragraph->id(),
'target_revision_id' => $paragraph->getRevisionId(),
];
- $entity->set($entity_field_name, $new_value);
+ $parent_entity->set($parent_entity_field_name, $new_value);
if ($save_entity) {
- $entity->save();
+ $parent_entity->save();
}
- static::$paragraphs[] = $paragraph;
+ static::$paragraphEntities[] = $paragraph;
return $paragraph;
}
/**
- * Find entity using provided conditions.
+ * Find entity.
+ *
+ * @param string $entity_type
+ * Entity type.
+ * @param string $bundle
+ * Bundle name.
+ * @param string $field_name
+ * Field name.
+ * @param string $field_value
+ * Field value.
+ *
+ * @return \Drupal\Core\Entity\ContentEntityInterface|null
+ * Found entity or NULL if not found.
*/
- protected function paragraphsFindEntity(array $conditions = []): ContentEntityInterface|null {
- $type = ($conditions['entity_type'] === 'taxonomy_term') ? 'vid' : 'type';
- $query = \Drupal::entityQuery($conditions['entity_type'])
+ protected function paragraphsFindEntity(string $entity_type, string $bundle, string $field_name, string $field_value): ?ContentEntityInterface {
+ $query = \Drupal::entityQuery($entity_type)
->accessCheck(FALSE)
- ->condition($type, $conditions['bundle'])
- ->condition($conditions['field_name'], $conditions['field_value']);
+ ->condition($entity_type === 'taxonomy_term' ? 'vid' : 'type', $bundle)
+ ->condition($field_name, $field_value);
$entity_ids = $query->execute();
if (empty($entity_ids)) {
- throw new \Exception(sprintf('Unable to find entity that matches conditions: "%s"', print_r($conditions, TRUE)));
+ return NULL;
}
$entity_id = array_pop($entity_ids);
- $entity = \Drupal::entityTypeManager()->getStorage($conditions['entity_type'])->load($entity_id);
+ $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id);
- if (!$entity instanceof ContentEntityInterface) {
- throw new \Exception(sprintf('Unable to load entity "%s" with id "%s"', $conditions['entity_type'], $entity_id));
- }
-
- return $entity;
+ return $entity instanceof ContentEntityInterface ? $entity : NULL;
}
/**
* Expand parsed fields into expected field values based on field type.
+ *
+ * @param string $entity_type
+ * Entity type.
+ * @param \StdClass $stub
+ * Stub object.
*/
protected function paragraphsExpandEntityFields(string $entity_type, \StdClass $stub): void {
$core = $this->getDriver()->getCore();
@@ -165,7 +171,17 @@ protected function paragraphsExpandEntityFields(string $entity_type, \StdClass $
}
/**
- * Get a field name that references the paragraphs item.
+ * Validate that an entity has a field.
+ *
+ * @param string $entity_type
+ * Entity type.
+ * @param string $bundle
+ * Bundle name.
+ * @param string $field_name
+ * Field name.
+ *
+ * @throws \RuntimeException
+ * If the field does not exist on the entity.
*/
protected function paragraphsValidateEntityFieldName(string $entity_type, string $bundle, string $field_name): void {
/** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_info */
diff --git a/src/PathTrait.php b/src/PathTrait.php
index d6cb1bf..27418c1 100644
--- a/src/PathTrait.php
+++ b/src/PathTrait.php
@@ -27,8 +27,17 @@ trait PathTrait {
*/
public function pathAssertCurrent(string $path): void {
$current_path = $this->getSession()->getCurrentUrl();
+ if (empty($current_path)) {
+ throw new \Exception('Current path is empty');
+ }
+
$current_path = parse_url((string) $current_path, PHP_URL_PATH);
- $current_path = ltrim($current_path, '/');
+
+ if ($current_path === FALSE) {
+ throw new \Exception('Current path is not a valid URL');
+ }
+
+ $current_path = ltrim((string) $current_path, '/');
$current_path = $current_path === '' ? '' : $current_path;
if ($current_path !== ltrim($path, '/')) {
@@ -50,8 +59,17 @@ public function pathAssertCurrent(string $path): void {
*/
public function pathAssertNotCurrent(string $path): bool {
$current_path = $this->getSession()->getCurrentUrl();
+ if (empty($current_path)) {
+ throw new \Exception('Current path is empty');
+ }
+
$current_path = parse_url((string) $current_path, PHP_URL_PATH);
- $current_path = ltrim($current_path, '/');
+
+ if ($current_path === FALSE) {
+ throw new \Exception('Current path is not a valid URL');
+ }
+
+ $current_path = ltrim((string) $current_path, '/');
$current_path = $current_path === '' ? '' : $current_path;
if ($current_path === $path) {
diff --git a/src/RoleTrait.php b/src/RoleTrait.php
index 5f5e7d3..15dba2e 100644
--- a/src/RoleTrait.php
+++ b/src/RoleTrait.php
@@ -19,15 +19,17 @@ trait RoleTrait {
/**
* Roles ids.
+ *
+ * @var array
*/
- protected array $rolesNeedClean = [];
+ protected array $rolesIds = [];
/**
* Create a single role with specified permissions.
*
* @Given role :name with permissions :permissions
*/
- public function roleCreateSingle(string $name, string $permissions) {
+ public function roleCreateSingle(string $name, string $permissions): void {
$permissions = array_map(trim(...), explode(',', $permissions));
$rid = strtolower($name);
@@ -38,22 +40,19 @@ public function roleCreateSingle(string $name, string $permissions) {
$existing_role->delete();
}
+ /** @var \Drupal\user\RoleInterface $role */
$role = \Drupal::entityTypeManager()->getStorage('user_role')->create([
'id' => $rid,
'label' => $name,
]);
$saved = $role->save();
- if ($saved === SAVED_NEW) {
- user_role_grant_permissions($role->id(), $permissions);
- // Mark for clean later.
- $this->rolesNeedClean[$role->id()] = $role->id();
-
- return Role::load($role->id());
-
+ if ($saved !== SAVED_NEW) {
+ throw new \RuntimeException(sprintf('Failed to create a role with "%s" permission(s).', implode(', ', $permissions)));
}
+ $this->rolesIds[(string) $role->id()] = (string) $role->id();
- throw new \RuntimeException(sprintf('Failed to create a role with "%s" permission(s).', implode(', ', $permissions)));
+ user_role_grant_permissions($role->id(), $permissions);
}
/**
@@ -83,14 +82,14 @@ public function roleCleanAll(AfterScenarioScope $scope): void {
return;
}
- foreach ($this->rolesNeedClean as $rid) {
+ foreach ($this->rolesIds as $rid) {
$role = Role::load($rid);
if ($role) {
$role->delete();
}
}
- $this->rolesNeedClean = [];
+ $this->rolesIds = [];
}
}
diff --git a/src/SearchApiTrait.php b/src/SearchApiTrait.php
index 2de1e20..442f822 100644
--- a/src/SearchApiTrait.php
+++ b/src/SearchApiTrait.php
@@ -25,7 +25,7 @@ trait SearchApiTrait {
* @When I index :type :title for search
*/
public function searchApiIndexContent(string $type, string $title): void {
- $nids = $this->contentNodeLoadMultiple($type, [
+ $nids = $this->contentLoadMultiple($type, [
'title' => $title,
]);
diff --git a/src/TaxonomyTrait.php b/src/TaxonomyTrait.php
index c34e464..15ac201 100644
--- a/src/TaxonomyTrait.php
+++ b/src/TaxonomyTrait.php
@@ -82,6 +82,7 @@ public function taxonomyDeleteTerms(string $vocabulary, TableNode $termsTable):
'name' => $name,
'vid' => $vocabulary,
]);
+
/** @var \Drupal\taxonomy\Entity\Term $term */
foreach ($terms as $term) {
$term->delete();
@@ -104,10 +105,11 @@ public function taxonomyVisitTermPageWithName(string $vocabulary, string $name):
}
ksort($tids);
-
$tid = end($tids);
+
$path = $this->locatePath('/taxonomy/term/' . $tid);
print $path;
+
$this->getSession()->visit($path);
}
@@ -126,10 +128,11 @@ public function taxonomyEditTermPageWithName(string $vocabulary, string $name):
}
ksort($tids);
-
$tid = end($tids);
+
$path = $this->locatePath('/taxonomy/term/' . $tid . '/edit');
print $path;
+
$this->getSession()->visit($path);
}
@@ -138,13 +141,13 @@ public function taxonomyEditTermPageWithName(string $vocabulary, string $name):
*
* @param string $vocabulary
* The term vocabulary.
- * @param array $conditions
+ * @param array $conditions
* Conditions keyed by field names.
*
- * @return array
+ * @return array
* Array of term ids.
*/
- protected function taxonomyLoadMultiple(string $vocabulary, array $conditions = []): int|array {
+ protected function taxonomyLoadMultiple(string $vocabulary, array $conditions = []): array {
$query = \Drupal::entityQuery('taxonomy_term')
->accessCheck(FALSE)
->condition('vid', $vocabulary);
diff --git a/src/UserTrait.php b/src/UserTrait.php
index b8c3864..0e2c749 100644
--- a/src/UserTrait.php
+++ b/src/UserTrait.php
@@ -6,7 +6,6 @@
use Behat\Gherkin\Node\TableNode;
use Drupal\user\Entity\User;
-use Drupal\user\UserInterface;
/**
* Trait UserTrait.
@@ -25,7 +24,14 @@ trait UserTrait {
* @When I visit user :name profile
*/
public function userVisitProfile(string $name): void {
- $user = $this->userGetByName($name);
+ $users = $this->userLoadMultiple(['name' => $name]);
+
+ if (empty($users)) {
+ throw new \RuntimeException(sprintf('User "%s" does not exist.', $name));
+ }
+
+ $user = reset($users);
+
$this->visitPath('/user/' . $user->id());
}
@@ -53,7 +59,14 @@ public function userVisitOwnProfile(): void {
* @When I edit user :name profile
*/
public function userEditProfile(string $name): void {
- $user = $this->userGetByName($name);
+ $users = $this->userLoadMultiple(['name' => $name]);
+
+ if (empty($users)) {
+ throw new \RuntimeException(sprintf('User "%s" does not exist.', $name));
+ }
+
+ $user = reset($users);
+
$this->visitPath('/user/' . $user->id() . '/edit');
}
@@ -64,20 +77,17 @@ public function userEditProfile(string $name): void {
*/
public function userDelete(TableNode $usersTable): void {
foreach ($usersTable->getHash() as $userHash) {
- $user = NULL;
- try {
- if (isset($userHash['mail'])) {
- $user = $this->userGetByMail($userHash['mail']);
- }
- elseif (isset($userHash['name'])) {
- $user = $this->userGetByName($userHash['name']);
- }
+ $users = [];
+
+ if (isset($userHash['mail'])) {
+ $users = $this->userLoadMultiple(['mail' => $userHash['mail']]);
}
- catch (\Exception) {
- // User may not exist - do nothing.
+ elseif (isset($userHash['name'])) {
+ $users = $this->userLoadMultiple(['name' => $userHash['name']]);
}
- if ($user) {
+ if (!empty($users)) {
+ $user = reset($users);
$user->delete();
$this->getUserManager()->removeUser($user->getAccountName());
}
@@ -90,7 +100,13 @@ public function userDelete(TableNode $usersTable): void {
* @Then user :name has :roles role(s) assigned
*/
public function userAssertHasRoles(string $name, string $roles): void {
- $user = $this->userGetByName($name);
+ $users = $this->userLoadMultiple(['name' => $name]);
+
+ if (empty($users)) {
+ throw new \RuntimeException(sprintf('User "%s" does not exist.', $name));
+ }
+
+ $user = reset($users);
$roles = explode(',', $roles);
$roles = array_map(function ($value): string {
@@ -108,7 +124,13 @@ public function userAssertHasRoles(string $name, string $roles): void {
* @Then user :name does not have :roles role(s) assigned
*/
public function userAssertHasNoRoles(string $name, string $roles): void {
- $user = $this->userGetByName($name);
+ $users = $this->userLoadMultiple(['name' => $name]);
+
+ if (empty($users)) {
+ throw new \RuntimeException(sprintf('User "%s" does not exist.', $name));
+ }
+
+ $user = reset($users);
$roles = explode(',', $roles);
$roles = array_map(function ($value): string {
@@ -126,12 +148,25 @@ public function userAssertHasNoRoles(string $name, string $roles): void {
* @Then user :name has :status status
*/
public function userAssertHasStatus(string $name, string $status): void {
- $status = $status === 'active';
+ if (!in_array($status, ['active', 'blocked'])) {
+ throw new \Exception(sprintf('Invalid status "%s".', $status));
+ }
+
+ $users = $this->userLoadMultiple(['name' => $name]);
- $user = $this->userGetByName($name);
+ if (empty($users)) {
+ throw new \RuntimeException(sprintf('User "%s" does not exist.', $name));
+ }
+
+ $user = reset($users);
- if ($user->isActive() != $status) {
- throw new \Exception(sprintf('User "%s" is expected to have status "%s", but has status "%s".', $name, $status ? 'active' : 'blocked', $user->isActive() ? 'active' : 'blocked'));
+ if ($status === 'active') {
+ if (!$user->isActive()) {
+ throw new \Exception(sprintf('User "%s" is expected to have status "active", but has status "blocked".', $name));
+ }
+ }
+ elseif ($user->isActive()) {
+ throw new \Exception(sprintf('User "%s" is expected to have status "blocked", but has status "active".', $name));
}
}
@@ -145,19 +180,17 @@ public function userSetPassword(string $name, string $password): void {
throw new \RuntimeException('Password must be not empty.');
}
- try {
- /** @var \Drupal\user\UserInterface $user */
- $user = $this->userGetByName($name);
+ $users = $this->userLoadMultiple(['name' => $name]);
+ if (empty($users)) {
+ $users = $this->userLoadMultiple(['mail' => $name]);
}
- catch (\Exception) {
- try {
- $user = $this->userGetByMail($name);
- }
- catch (\Exception) {
- throw new \Exception(sprintf('Unable to find a user with name or email "%s".', $name));
- }
+
+ if (empty($users)) {
+ throw new \RuntimeException(sprintf('Unable to find a user with name or email "%s".', $name));
}
+ $user = reset($users);
+
$user->setPassword($password)->save();
}
@@ -167,47 +200,25 @@ public function userSetPassword(string $name, string $password): void {
* @Then the last access time of user :name is :time
*/
public function setUserLastAccess(string $name, string $time): void {
- /** @var \Drupal\user\UserInterface $user */
- $user = $this->userGetByName($name);
- $timestamp = (int) static::dateRelativeProcessValue($time, time());
- $user->setLastAccessTime($timestamp)->save();
- }
-
- /**
- * Get user by name.
- */
- protected function userGetByName(string $name): UserInterface {
$users = $this->userLoadMultiple(['name' => $name]);
- $user = reset($users);
- if (!$user) {
- throw new \RuntimeException(sprintf('Unable to find user with name "%s".', $name));
+ if (empty($users)) {
+ throw new \RuntimeException(sprintf('User "%s" does not exist.', $name));
}
- return $user;
- }
-
- /**
- * Get user by mail.
- */
- protected function userGetByMail(string $mail) {
- $users = $this->userLoadMultiple(['mail' => $mail]);
$user = reset($users);
- if (!$user) {
- throw new \RuntimeException(sprintf('Unable to find user with mail "%s".', $mail));
- }
-
- return $user;
+ $timestamp = (int) static::dateRelativeProcessValue($time, time());
+ $user->setLastAccessTime($timestamp)->save();
}
/**
* Load multiple users with specified conditions.
*
- * @param array $conditions
+ * @param array $conditions
* Conditions keyed by field names.
*
- * @return array
+ * @return array
* Array of loaded user objects.
*/
protected function userLoadMultiple(array $conditions = []): array {
diff --git a/src/WatchdogTrait.php b/src/WatchdogTrait.php
index e183023..6135815 100644
--- a/src/WatchdogTrait.php
+++ b/src/WatchdogTrait.php
@@ -27,7 +27,7 @@ trait WatchdogTrait {
/**
* Array of watchdog message types.
*
- * @var array
+ * @var array
*/
protected $watchdogMessageTypes = [];
@@ -54,12 +54,12 @@ public function watchdogSetScenarioStartTime(BeforeScenarioScope $scope): void {
* @watchdog:my_module_type @watchdog:my_other_module_type
* @endcode
*
- * @param array $tags
+ * @param array $tags
* Array of scenario tags.
* @param string $prefix
* Optional tag prefix to filter by.
*
- * @return array
+ * @return array
* Array of message types. 'php' is always added to the list.
*/
protected function watchdogParseMessageTypes(array $tags = [], string $prefix = 'watchdog:'): array {
diff --git a/src/WysiwygTrait.php b/src/WysiwygTrait.php
index 67a3d1b..02af5e0 100644
--- a/src/WysiwygTrait.php
+++ b/src/WysiwygTrait.php
@@ -83,7 +83,7 @@ public function wysiwygFillField(string $field, string $value): void {
/**
* Returns fixed step argument (with \\" replaced back to ").
*/
- protected function wysiwygFixStepArgument(string $argument): string|array {
+ protected function wysiwygFixStepArgument(string $argument): string {
return str_replace('\\"', '"', $argument);
}
diff --git a/tests/behat/bootstrap/BehatCliTrait.php b/tests/behat/bootstrap/BehatCliTrait.php
index 37c2e87..e371a98 100644
--- a/tests/behat/bootstrap/BehatCliTrait.php
+++ b/tests/behat/bootstrap/BehatCliTrait.php
@@ -7,6 +7,8 @@
* phpcs:disable Drupal.Commenting.DocComment.MissingShort
*/
+declare(strict_types=1);
+
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\PyStringNode;
use PHPUnit\Framework\Assert;
@@ -21,16 +23,16 @@ trait BehatCliTrait {
/**
* @BeforeScenario
*/
- public function behatCliBeforeScenario(BeforeScenarioScope $scope) {
+ public function behatCliBeforeScenario(BeforeScenarioScope $scope): void {
$traits = [];
// Scan scenario tags and extract trait names from tags starting with
// 'trait:'. For example, @trait:PathTrait or @trait:D7\\UserTrait.
foreach ($scope->getScenario()->getTags() as $tag) {
- if (strpos($tag, 'trait:') === 0) {
+ if (str_starts_with($tag, 'trait:')) {
$tags = trim(substr($tag, strlen('trait:')));
$tags = explode(',', $tags);
- $tags = array_map(function ($value) {
+ $tags = array_map(function ($value): string {
return trim(str_replace('\\\\', '\\', $value));
}, $tags);
$traits = array_merge($traits, $tags);
@@ -52,7 +54,7 @@ public function behatCliBeforeScenario(BeforeScenarioScope $scope) {
/**
* @BeforeStep
*/
- public function behatCliBeforeStep() {
+ public function behatCliBeforeStep(): void {
// Drupal Extension >= ^5 is coupled with Drupal core's DrupalTestBrowser.
// This requires Drupal root to be discoverable when running Behat from a
// random directory using Drupal Finder.
@@ -74,14 +76,14 @@ public function behatCliBeforeStep() {
* @return string
* Path to written file.
*/
- public function behatCliWriteFeatureContextFile(array $traits = []) {
+ public function behatCliWriteFeatureContextFile(array $traits = []): string {
$tokens = [
'{{USE_DECLARATION}}' => '',
'{{USE_IN_CLASS}}' => '',
];
foreach ($traits as $trait) {
$tokens['{{USE_DECLARATION}}'] .= sprintf('use DrevOps\\BehatSteps\\%s;' . PHP_EOL, $trait);
- $trait_name__parts = explode('\\', $trait);
+ $trait_name__parts = explode('\\', (string) $trait);
$trait_name = end($trait_name__parts);
$tokens['{{USE_IN_CLASS}}'] .= sprintf('use %s;' . PHP_EOL, $trait_name);
}
@@ -116,7 +118,7 @@ public function setWatchdogErrorDrupal7($level, $type = 'php') {
* @Given set watchdog error level :level
* @Given set watchdog error level :level of type :type
*/
- public function setWatchdogErrorDrupal9($level, $type = 'php') {
+ public function testSetWatchdogError($level, $type = 'php') {
\Drupal::logger($type)->log($level, 'test');
}
@@ -146,7 +148,7 @@ public function setWatchdogErrorDrupal9($level, $type = 'php') {
/**
* @Given /^scenario steps(?: tagged with "([^"]*)")?:$/
*/
- public function behatCliWriteScenarioSteps(PyStringNode $content, $tags = '') {
+ public function behatCliWriteScenarioSteps(PyStringNode $content, $tags = ''): void {
$content = strtr((string) $content, ["'''" => '"""']);
// Make sure that indentation in provided content is accurate.
@@ -182,7 +184,7 @@ public function behatCliWriteScenarioSteps(PyStringNode $content, $tags = '') {
/**
* @Given some behat configuration
*/
- public function behatCliWriteBehatYml() {
+ public function behatCliWriteBehatYml(): void {
$content = <<<'EOL'
default:
suites:
@@ -218,7 +220,7 @@ public function behatCliWriteBehatYml() {
/**
* @Then it should fail with an error:
*/
- public function behatCliAssertFailWithError(PyStringNode $message) {
+ public function behatCliAssertFailWithError(PyStringNode $message): void {
$this->itShouldFail('fail');
Assert::assertStringContainsString(trim((string) $message), $this->getOutput());
// Enforce \Exception for all assertion exceptions. Non-assertion
@@ -230,7 +232,7 @@ public function behatCliAssertFailWithError(PyStringNode $message) {
/**
* @Then it should fail with an exception:
*/
- public function behatCliAssertFailWithException(PyStringNode $message) {
+ public function behatCliAssertFailWithException(PyStringNode $message): void {
$this->itShouldFail('fail');
Assert::assertStringContainsString(trim((string) $message), $this->getOutput());
// Enforce \RuntimeException for all non-assertion exceptions. Assertion
@@ -242,24 +244,24 @@ public function behatCliAssertFailWithException(PyStringNode $message) {
/**
* Helper to print file comments.
*/
- protected static function behatCliPrintFileContents($filename, $title = '') {
+ protected static function behatCliPrintFileContents(string $filename, $title = '') {
if (!is_readable($filename)) {
throw new \RuntimeException(sprintf('Unable to access file "%s"', $filename));
}
$content = file_get_contents($filename);
- print "-------------------- $title START --------------------" . PHP_EOL;
+ print sprintf('-------------------- %s START --------------------', $title) . PHP_EOL;
print $filename . PHP_EOL;
print_r($content);
print PHP_EOL;
- print "-------------------- $title FINISH --------------------" . PHP_EOL;
+ print sprintf('-------------------- %s FINISH --------------------', $title) . PHP_EOL;
}
/**
* Helper to check if debug mode is enabled.
*/
- protected static function behatCliIsDebug() {
+ protected static function behatCliIsDebug(): bool {
// Change to TRUE to see debug messages for this trait.
return FALSE;
}
diff --git a/tests/behat/bootstrap/FeatureContext.php b/tests/behat/bootstrap/FeatureContext.php
index d76b28b..2577c98 100644
--- a/tests/behat/bootstrap/FeatureContext.php
+++ b/tests/behat/bootstrap/FeatureContext.php
@@ -5,6 +5,8 @@
* Feature context for testing Behat-steps.
*/
+declare(strict_types=1);
+
use DrevOps\BehatSteps\BigPipeTrait;
use DrevOps\BehatSteps\ContentTrait;
use DrevOps\BehatSteps\CookieTrait;
@@ -83,7 +85,7 @@ class FeatureContext extends DrupalContext {
* This cannot be moved to FeatureContextTrait because traits cannot override
* methods from other traits.
*/
- public static function dateNow(): int {
+ protected static function dateNow(): int {
return strtotime('2024-07-15 12:00:00');
}
diff --git a/tests/behat/bootstrap/FeatureContextTrait.php b/tests/behat/bootstrap/FeatureContextTrait.php
index a522fcf..922b9b3 100644
--- a/tests/behat/bootstrap/FeatureContextTrait.php
+++ b/tests/behat/bootstrap/FeatureContextTrait.php
@@ -10,6 +10,8 @@
* phpcs:disable Drupal.Commenting.DocComment.MissingShort
*/
+declare(strict_types=1);
+
use Behat\Behat\Hook\Scope\AfterFeatureScope;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Mink\Driver\Selenium2Driver;
@@ -30,7 +32,7 @@ trait FeatureContextTrait {
*
* @AfterFeature @errorcleanup
*/
- public static function cleanWatchdog(AfterFeatureScope $scope) {
+ public static function testClearWatchdog(AfterFeatureScope $scope): void {
$database = Database::getConnection();
if ($database->schema()->tableExists('watchdog')) {
$database->truncate('watchdog')->execute();
@@ -40,7 +42,7 @@ public static function cleanWatchdog(AfterFeatureScope $scope) {
/**
* @Then user :name does not exist
*/
- public function userDoesNotExist($name) {
+ public function testAssertUserDoesNotExist(string $name): void {
// We need to check that user was removed from both DB and test variables.
$users = $this->userLoadMultiple(['name' => $name]);
$user = reset($users);
@@ -52,7 +54,7 @@ public function userDoesNotExist($name) {
try {
$this->getUserManager()->getUser($name);
}
- catch (\Exception $exception) {
+ catch (\Exception) {
return;
}
@@ -63,47 +65,26 @@ public function userDoesNotExist($name) {
* @Given set watchdog error level :level
* @Given set watchdog error level :level of type :type
*/
- public function setWatchdogErrorDrupal9($level, $type = 'php') {
+ public function testSetWatchdogError(string $level, string $type = 'php'): void {
\Drupal::logger($type)->log($level, 'test');
}
/**
* @Given cookie :name exists
*/
- public function assertCookieExists($name) {
- $cookies = $this->getCookies();
+ public function testAssertCookieExists(string $name): void {
+ $cookies = $this->testGetAllCookies();
if (!isset($cookies[$name])) {
throw new \Exception(sprintf('Cookie "%s" does not exist.', $name));
}
}
- /**
- * Get a list of cookies.
- */
- protected function getCookies() {
- $cookie_list = [];
-
- $driver = $this->getSession()->getDriver();
- if ($driver instanceof Selenium2Driver) {
- $cookies = $driver->getWebDriverSession()->getAllCookies();
- foreach ($cookies as $cookie) {
- $cookie_list[$cookie['name']] = $cookie['value'];
- }
- }
- else {
- /** @var \Behat\Mink\Driver\BrowserKitDriver $driver */
- $cookie_list = $driver->getClient()->getCookieJar()->allValues($driver->getCurrentUrl());
- }
-
- return $cookie_list;
- }
-
/**
* @Given cookie :name does not exist
*/
- public function assertCookieNotExists($name) {
- $cookies = $this->getCookies();
+ public function testAssertCookieNotExists(string $name): void {
+ $cookies = $this->testGetAllCookies();
if (isset($cookies[$name])) {
throw new \Exception(sprintf('Cookie "%s" exists but should not.', $name));
@@ -113,7 +94,7 @@ public function assertCookieNotExists($name) {
/**
* @Given I install a :name module
*/
- public function installModule($name) {
+ public function testInstallModule(string $name): void {
/** @var \Drupal\Core\Extension\ModuleHandler $module_handler */
$module_handler = \Drupal::service('module_handler');
if ($module_handler->moduleExists($name)) {
@@ -126,8 +107,8 @@ public function installModule($name) {
try {
$result = $module_installer->install([$name]);
}
- catch (MissingDependencyException $exception) {
- throw new \Exception(sprintf('Unable to install a module "%s": %s.', $name, $exception->getMessage()));
+ catch (MissingDependencyException $missingDependencyException) {
+ throw new \Exception(sprintf('Unable to install a module "%s": %s.', $name, $missingDependencyException->getMessage()), $missingDependencyException->getCode(), $missingDependencyException);
}
if (!$result) {
@@ -138,7 +119,7 @@ public function installModule($name) {
/**
* @Given I uninstall a :name module
*/
- public function uninstallModule($name) {
+ public function testUninstallModule(string $name): void {
/** @var \Drupal\Core\Extension\ModuleHandler $module_handler */
$module_handler = \Drupal::service('module_handler');
if (!$module_handler->moduleExists($name)) {
@@ -159,7 +140,7 @@ public function uninstallModule($name) {
* @When I send test email to :email with
* @When I send test email to :email with:
*/
- public function sendTestEmail($email, PyStringNode $string) {
+ public function testSendEmail(string $email, PyStringNode $string): void {
\Drupal::service('plugin.manager.mail')->mail(
'mysite_core',
'test_email',
@@ -173,7 +154,7 @@ public function sendTestEmail($email, PyStringNode $string) {
/**
* @Then :file_name file object exists
*/
- public function fileObjectExist($file_name) {
+ public function testAssertFileObjectExists(string $file_name): void {
$file_name = basename($file_name);
$fids = $this->fileLoadMultiple(['filename' => $file_name]);
if (empty($fids)) {
@@ -191,7 +172,7 @@ public function fileObjectExist($file_name) {
/**
* @Then no :file_name file object exists
*/
- public function noFileObjectExist($file_name) {
+ public function testAssertFileObjectNotExists(string $file_name): void {
$file_name = basename($file_name);
$fids = $this->fileLoadMultiple(['filename' => $file_name]);
if ($fids) {
@@ -202,7 +183,7 @@ public function noFileObjectExist($file_name) {
/**
* @Then :entity_type entity exists with UUID :uuid
*/
- public function entityExistsByUuid($entity_type, $uuid) {
+ public function testAssertEntityExistsByUuid(string $entity_type, string $uuid): void {
$entity = \Drupal::service('entity.repository')->loadEntityByUuid($entity_type, $uuid);
if (!$entity) {
@@ -210,6 +191,30 @@ public function entityExistsByUuid($entity_type, $uuid) {
}
}
+ /**
+ * Get a list of cookies.
+ *
+ * @return array
+ * List of cookies.
+ */
+ protected function testGetAllCookies(): array {
+ $cookie_list = [];
+
+ $driver = $this->getSession()->getDriver();
+ if ($driver instanceof Selenium2Driver) {
+ $cookies = $driver->getWebDriverSession()->getAllCookies();
+ foreach ($cookies as $cookie) {
+ $cookie_list[$cookie['name']] = $cookie['value'];
+ }
+ }
+ else {
+ /** @var \Behat\Mink\Driver\BrowserKitDriver $driver */
+ $cookie_list = $driver->getClient()->getCookieJar()->allValues($driver->getCurrentUrl());
+ }
+
+ return $cookie_list;
+ }
+
/**
* @Given I set a test cookie with name :name and value :value
*/
diff --git a/tests/behat/features/big_pipe.feature b/tests/behat/features/big_pipe.feature
index 1ec270e..911727a 100644
--- a/tests/behat/features/big_pipe.feature
+++ b/tests/behat/features/big_pipe.feature
@@ -1,4 +1,4 @@
-Feature: Check that BigPipeTrait works for or D9
+Feature: Check that BigPipeTrait works
@api
Scenario: Assert that Big Pipe cookie is set
diff --git a/tests/behat/features/content.feature b/tests/behat/features/content.feature
index dfeefed..cb9a3a1 100644
--- a/tests/behat/features/content.feature
+++ b/tests/behat/features/content.feature
@@ -1,4 +1,4 @@
-Feature: Check that ContentTrait works for or D9
+Feature: Check that ContentTrait works
@api
Scenario: Assert visiting a page with title of specified content type
diff --git a/tests/behat/features/eck.feature b/tests/behat/features/eck.feature
index 9827655..f515cef 100644
--- a/tests/behat/features/eck.feature
+++ b/tests/behat/features/eck.feature
@@ -1,4 +1,4 @@
-Feature: Check that EckTrait works for or D9
+Feature: Check that EckTrait works
Background:
Given no test_bundle test_entity_type entities:
diff --git a/tests/behat/features/email.feature b/tests/behat/features/email.feature
index ae3b9dc..6516a37 100644
--- a/tests/behat/features/email.feature
+++ b/tests/behat/features/email.feature
@@ -1,4 +1,4 @@
-Feature: Check that email assertions work for or D9
+Feature: Check that EmailTrait works
@api @email
Scenario: As a developer, I want to know that test email system is automatically
diff --git a/tests/behat/features/file.feature b/tests/behat/features/file.feature
index 150076c..e10d70e 100644
--- a/tests/behat/features/file.feature
+++ b/tests/behat/features/file.feature
@@ -1,4 +1,4 @@
-Feature: Check that FileTrait works for or D9
+Feature: Check that FileTrait works
@api
Scenario: Assert "Given managed file:"
diff --git a/tests/behat/features/file_download.feature b/tests/behat/features/file_download.feature
index 87ed56b..dee27ab 100644
--- a/tests/behat/features/file_download.feature
+++ b/tests/behat/features/file_download.feature
@@ -1,4 +1,4 @@
-Feature: Check that FileDownloadTrait works for or D9
+Feature: Check that FileDownloadTrait works
Background:
Given I am logged in as a user with the "administrator" role
diff --git a/tests/behat/features/media.feature b/tests/behat/features/media.feature
index 8b9e23b..9f414cb 100644
--- a/tests/behat/features/media.feature
+++ b/tests/behat/features/media.feature
@@ -1,4 +1,4 @@
-Feature: Check that MediaTrait works for or D9
+Feature: Check that MediaTrait works
@api
Scenario: Assert "When I attach the file :file to :field_name media field"
diff --git a/tests/behat/features/menu.feature b/tests/behat/features/menu.feature
index f9853be..5b3ae45 100644
--- a/tests/behat/features/menu.feature
+++ b/tests/behat/features/menu.feature
@@ -1,4 +1,4 @@
-Feature: Check that MenuTrait works for or D9
+Feature: Check that MenuTrait works
@api
Scenario: Assert "Given menus:"
diff --git a/tests/behat/features/override.feature b/tests/behat/features/override.feature
index be11b89..cb3bf3f 100644
--- a/tests/behat/features/override.feature
+++ b/tests/behat/features/override.feature
@@ -1,4 +1,4 @@
-Feature: Check that OverrideTrait works for or D9
+Feature: Check that OverrideTrait works
@api
Scenario Outline: Assert override of authentication by role works
diff --git a/tests/behat/features/paragraphs.feature b/tests/behat/features/paragraphs.feature
index d48e612..5cdd0a9 100644
--- a/tests/behat/features/paragraphs.feature
+++ b/tests/behat/features/paragraphs.feature
@@ -1,4 +1,4 @@
-Feature: Check that ParagraphsTrait works for or D9
+Feature: Check that ParagraphsTrait works
Background:
Given I am logged in as a user with the "administrator" role
diff --git a/tests/behat/features/role.feature b/tests/behat/features/role.feature
index 2bc403b..b02127e 100644
--- a/tests/behat/features/role.feature
+++ b/tests/behat/features/role.feature
@@ -1,4 +1,4 @@
-Feature: Check that RoleTrait works for or D9
+Feature: Check that RoleTrait works
@api
Scenario: Assert "Given role :name with permissions :permissions"
diff --git a/tests/behat/features/taxonomy.feature b/tests/behat/features/taxonomy.feature
index bd81147..bda6b64 100644
--- a/tests/behat/features/taxonomy.feature
+++ b/tests/behat/features/taxonomy.feature
@@ -1,4 +1,4 @@
-Feature: Check that TaxonomyTrait works for or D9
+Feature: Check that TaxonomyTrait works
@api
Scenario: Assert "Given vocabulary :vid with name :name exists"
diff --git a/tests/behat/features/user.feature b/tests/behat/features/user.feature
index c2d7bd4..14638b2 100644
--- a/tests/behat/features/user.feature
+++ b/tests/behat/features/user.feature
@@ -1,4 +1,4 @@
-Feature: Check that UserTrait works for or D9
+Feature: Check that UserTrait works
Background:
Given users:
@@ -64,9 +64,7 @@ Feature: Check that UserTrait works for or D9
@api
Scenario: Assert "Then user :name has :status status"
Given user "authenticated_user" has "active" status
-
- Given user "authenticated_user_disabled" has "not active" status
- And user "authenticated_user_disabled" has "disabled" status
+ And user "authenticated_user_disabled" has "blocked" status
@api
Scenario: Assert "I set user :user password to :password"
@@ -81,7 +79,7 @@ Feature: Check that UserTrait works for or D9
Given I set user "non_existing_user" password to "password123"
"""
When I run "behat --no-colors"
- Then it should fail with an error:
+ Then it should fail with an exception:
"""
Unable to find a user with name or email "non_existing_user".
"""
diff --git a/tests/behat/features/watchdog.feature b/tests/behat/features/watchdog.feature
index 48a2aec..dcde974 100644
--- a/tests/behat/features/watchdog.feature
+++ b/tests/behat/features/watchdog.feature
@@ -1,5 +1,5 @@
@errorcleanup
-Feature: Check that WatchdogTrait works for or D9
+Feature: Check that WatchdogTrait works
@trait:WatchdogTrait
Scenario: Assert that watchdog fails with an error