|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace DrevOps\BehatSteps; |
| 6 | + |
| 7 | +use Behat\Behat\Hook\Scope\AfterScenarioScope; |
| 8 | +use Drupal\block_content\Entity\BlockContent; |
| 9 | +use Behat\Gherkin\Node\TableNode; |
| 10 | +use Drupal\block_content\BlockContentTypeInterface; |
| 11 | + |
| 12 | +/** |
| 13 | + * Provides Behat step definitions for managing custom block content entities. |
| 14 | + * |
| 15 | + * This trait enables programmatic management of custom block_content |
| 16 | + * entities in Drupal, including creation, validation, and editing operations. |
| 17 | + * These reusable content blocks can be placed in regions using the BlockTrait. |
| 18 | + */ |
| 19 | +trait BlockContentTrait { |
| 20 | + |
| 21 | + /** |
| 22 | + * Array of created block_content entities. |
| 23 | + * |
| 24 | + * @var array<int,\Drupal\block_content\Entity\BlockContent> |
| 25 | + */ |
| 26 | + protected static $blockContentEntities = []; |
| 27 | + |
| 28 | + /** |
| 29 | + * Cleans up all custom block content entities created during the scenario. |
| 30 | + * |
| 31 | + * This method automatically runs after each scenario to ensure clean test |
| 32 | + * state. |
| 33 | + * Add the tag @behat-steps-skip:blockContentCleanAll to your scenario to |
| 34 | + * prevent automatic cleanup of block content entities. |
| 35 | + * |
| 36 | + * @AfterScenario |
| 37 | + */ |
| 38 | + public function blockContentCleanAll(AfterScenarioScope $scope): void { |
| 39 | + // Allow to skip this by adding a tag. |
| 40 | + if ($scope->getScenario()->hasTag('behat-steps-skip:' . __FUNCTION__)) { |
| 41 | + return; |
| 42 | + } |
| 43 | + |
| 44 | + foreach (static::$blockContentEntities as $block_content) { |
| 45 | + $block_content->delete(); |
| 46 | + } |
| 47 | + static::$blockContentEntities = []; |
| 48 | + } |
| 49 | + |
| 50 | + /** |
| 51 | + * Verifies that a custom block type exists. |
| 52 | + * |
| 53 | + * @code |
| 54 | + * Given "search" block_content type exists |
| 55 | + * @endcode |
| 56 | + * |
| 57 | + * @Given :type block_content type exists |
| 58 | + */ |
| 59 | + public function blockContentAssertTypeExist(string $type): void { |
| 60 | + $block_content_type = \Drupal::entityTypeManager()->getStorage('block_content_type')->load($type); |
| 61 | + |
| 62 | + if (!$block_content_type instanceof BlockContentTypeInterface) { |
| 63 | + throw new \Exception(sprintf('"%s" block_content_type does not exist', $type)); |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + /** |
| 68 | + * Removes custom blocks of a specified type with the given descriptions. |
| 69 | + * |
| 70 | + * Deletes all custom blocks of the specified type that match any of the |
| 71 | + * descriptions (titles) provided in the table. |
| 72 | + * |
| 73 | + * @code |
| 74 | + * Given no "basic" block_content: |
| 75 | + * | [TEST] Footer Block | |
| 76 | + * | [TEST] Contact Form | |
| 77 | + * @endcode |
| 78 | + * |
| 79 | + * @Given no :type block_content: |
| 80 | + * |
| 81 | + * @throws \Drupal\Core\Entity\EntityStorageException |
| 82 | + * When the entity cannot be deleted. |
| 83 | + */ |
| 84 | + public function blockContentDelete(string $type, TableNode $contentBlockTable): void { |
| 85 | + foreach ($contentBlockTable->getColumn(0) as $description) { |
| 86 | + $content_blocks = \Drupal::entityTypeManager()->getStorage('block_content')->loadByProperties([ |
| 87 | + 'info' => $description, |
| 88 | + 'type' => $type, |
| 89 | + ]); |
| 90 | + |
| 91 | + foreach ($content_blocks as $content_block) { |
| 92 | + /** @var \Drupal\block_content\Entity\BlockContent $content_block */ |
| 93 | + $content_block->delete(); |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + /** |
| 99 | + * Navigates to the edit page for a specified custom block. |
| 100 | + * |
| 101 | + * Finds a custom block by its type and description (admin title) and |
| 102 | + * navigates to its edit page. Throws an exception if no matching block |
| 103 | + * is found. |
| 104 | + * |
| 105 | + * @code |
| 106 | + * When I edit "basic" block_content_type with description "[TEST] Footer Block" |
| 107 | + * @endcode |
| 108 | + * |
| 109 | + * @When I edit :type block_content_type with description :description |
| 110 | + */ |
| 111 | + public function blockContentEditBlockContentWithDescription(string $type, string $description): void { |
| 112 | + $block_ids = $this->blockContentLoadMultiple($type, [ |
| 113 | + 'info' => $description, |
| 114 | + ]); |
| 115 | + |
| 116 | + if (empty($block_ids)) { |
| 117 | + throw new \RuntimeException(sprintf('Unable to find %s block content with description "%s"', $type, $description)); |
| 118 | + } |
| 119 | + |
| 120 | + ksort($block_ids); |
| 121 | + $block_id = end($block_ids); |
| 122 | + |
| 123 | + $path = $this->locatePath('/admin/content/block/' . $block_id); |
| 124 | + $this->getSession()->visit($path); |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Load multiple content blocks with specified type and conditions. |
| 129 | + * |
| 130 | + * @param string $type |
| 131 | + * The block content type. |
| 132 | + * @param array<string,string> $conditions |
| 133 | + * Conditions keyed by field names. |
| 134 | + * |
| 135 | + * @return array<int, string> |
| 136 | + * Array of block content ids. |
| 137 | + */ |
| 138 | + protected function blockContentLoadMultiple(string $type, array $conditions = []): array { |
| 139 | + $query = \Drupal::entityQuery('block_content') |
| 140 | + ->accessCheck(FALSE) |
| 141 | + ->condition('type', $type); |
| 142 | + |
| 143 | + foreach ($conditions as $k => $v) { |
| 144 | + $and = $query->andConditionGroup(); |
| 145 | + $and->condition($k, $v); |
| 146 | + $query->condition($and); |
| 147 | + } |
| 148 | + |
| 149 | + return $query->execute(); |
| 150 | + } |
| 151 | + |
| 152 | + /** |
| 153 | + * Creates custom blocks of the specified type with the given field values. |
| 154 | + * |
| 155 | + * This step creates new custom block (block_content) entities with |
| 156 | + * the specified field values. |
| 157 | + * Each row in the table creates a separate block entity of the given type. |
| 158 | + * |
| 159 | + * Required fields: |
| 160 | + * - info (or title): The block's admin title/label |
| 161 | + * |
| 162 | + * Common optional fields: |
| 163 | + * - status: Published status (1 for published, 0 for unpublished) |
| 164 | + * - created: Creation timestamp (format: YYYY-MM-DD H:MMam/pm) |
| 165 | + * - body: Block content (for blocks with a body field) |
| 166 | + * |
| 167 | + * @param string $type |
| 168 | + * The custom block type machine name. |
| 169 | + * @param \Behat\Gherkin\Node\TableNode $block_content_table |
| 170 | + * Table containing field values for each block to create. |
| 171 | + * |
| 172 | + * @Given :type block_content: |
| 173 | + * |
| 174 | + * @code |
| 175 | + * Given "basic" block_content: |
| 176 | + * | info | status | body | created | |
| 177 | + * | [TEST] Footer Contact | 1 | Call us at 555-1234 | 2023-01-17 8:00am | |
| 178 | + * | [TEST] Copyright | 1 | © 2023 Example Company | 2023-01-18 9:00am | |
| 179 | + * @endcode |
| 180 | + */ |
| 181 | + public function blockContentCreate(string $type, TableNode $block_content_table): void { |
| 182 | + foreach ($block_content_table->getHash() as $blockContentHash) { |
| 183 | + $this->createBlockContent($type, $blockContentHash); |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + /** |
| 188 | + * Creates a block content entity with the specified type and field values. |
| 189 | + * |
| 190 | + * This internal helper method creates and saves a single block content |
| 191 | + * entity. |
| 192 | + * Created entities are stored in the static $blockContentEntities array for |
| 193 | + * automatic cleanup after the scenario. |
| 194 | + * |
| 195 | + * @param string $type |
| 196 | + * The machine name of the block content type. |
| 197 | + * @param array<string> $values |
| 198 | + * Associative array of field values for the block content entity. |
| 199 | + * Common fields include: |
| 200 | + * - info: The admin title/label (required) |
| 201 | + * - body: The body field value (optional) |
| 202 | + * - status: Published status (optional, 1 = published, 0 = unpublished) |
| 203 | + * |
| 204 | + * @return \Drupal\block_content\Entity\BlockContent |
| 205 | + * The created block content entity. |
| 206 | + * |
| 207 | + * @throws \Drupal\Core\Entity\EntityStorageException |
| 208 | + * When the entity cannot be saved. |
| 209 | + * |
| 210 | + * @SuppressWarnings(PHPMD.StaticAccess) |
| 211 | + */ |
| 212 | + protected function createBlockContent(string $type, array $values): BlockContent { |
| 213 | + $block_content = (object) $values; |
| 214 | + $block_content->type = $type; |
| 215 | + $this->parseEntityFields('block_content', $block_content); |
| 216 | + $block_content = (array) $block_content; |
| 217 | + /** @var \Drupal\block_content\Entity\BlockContent $block_content */ |
| 218 | + $block_content = BlockContent::create($block_content); |
| 219 | + $block_content->save(); |
| 220 | + static::$blockContentEntities[] = $block_content; |
| 221 | + |
| 222 | + return $block_content; |
| 223 | + } |
| 224 | + |
| 225 | +} |
0 commit comments