Skip to content

Commit 5e3c9ad

Browse files
richardgauntclaudeAlexSkrypnyk
authored
[#338] Added traits to create, configure blocks and block_content entities. (#337)
Co-authored-by: Claude <[email protected]> Co-authored-by: Alex Skrypnyk <[email protected]>
1 parent 4089069 commit 5e3c9ad

File tree

7 files changed

+908
-3
lines changed

7 files changed

+908
-3
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,27 @@ Modification of `behat.yml` configuration is not required.
6969
| `When the moderation state of :type :title changes from :old_state to :new_state` | Change the moderation state of content with the specified title. |
7070
| `When I visit :type :title scheduled transitions` | Visit the scheduled transition page for a node with the specified title. |
7171
| &nbsp; | |
72+
| **[`BlockTrait`](src/BlockTrait.php) ([example](tests/behat/features/block.feature))** | |
73+
| `When I create a block of type :label with:` | Configure an existing block with provided settings (label, region, status, etc.). |
74+
| `When I configure the block with the label :label with:` | Configure an existing block with provided settings (label, region, status, etc.). |
75+
| `When I configure a visibility condition :condition for the block with label :label` | Set a visibility condition for a block. |
76+
| `When I remove the visibility condition :condition from the block with label :label` | Remove a visibility condition from a block. |
77+
| `When I disable the block with label :label` | Disable a block specified with a label. |
78+
| `When I enable the block with label :label` | Enable a block specified with a label. |
79+
| `Then block with label :label should exist` | Assert that a block exists. |
80+
| `Then block with label :label should exist in the region :region` | Assert that a block exists in a specified region. |
81+
| `Then block with label :label should not exist in the region :region` | Assert that a block does not exist in a specified region. |
82+
| `Then the block with label :label should have the visibility condition :condition` | Assert that a block has a visibility condition. |
83+
| `Then the block with label :label should not have the visibility condition :condition` | Assert that a block does not have a visibility condition. |
84+
| `Then the block with label :label is disabled` | Assert that a block with specified label is disabled. |
85+
| `Then the block with label :label is enabled` | Assert that a block with specified label is enabled. |
86+
| &nbsp; | |
87+
| **[`BlockContentTrait`](src/BlockContentTrait.php) ([example](tests/behat/features/content_block.feature))** | |
88+
| `Given block_content_type :type exists` | Assert that a block content type exists. |
89+
| `Given :type block_content:` | Create block content of a given type with field data. |
90+
| `Given no :type block_content:` | Remove content blocks from a specified content block type. |
91+
| `When I edit :type block_content_type with description :description` | Navigate to specified block content type edit page. |
92+
| &nbsp; | |
7293
| **[`CookieTrait`](src/CookieTrait.php) ([example](tests/behat/features/cookie.feature))** | |
7394
| `Then a cookie with( the) name :name should exist` | Check if a cookie with the specified name exists. |
7495
| `Then a cookie with( the) name :name and value :value should exist` | Check if a cookie with the specified name and value exists. |

behat.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ default:
5555
root: web
5656
selectors:
5757
message_selector: '.messages'
58-
error_message_selector: '.messages.error'
59-
success_message_selector: '.messages.status'
60-
warning_message_selector: '.messages.warning'
58+
error_message_selector: '.messages.messages--error'
59+
success_message_selector: '.messages.messages--status'
60+
warning_message_selector: '.messages.messages--warning'
6161

6262
# Capture HTML and JPG screenshots on demand and on failure.
6363
DrevOps\BehatScreenshotExtension:

src/BlockContentTrait.php

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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

Comments
 (0)