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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions CONTRIBUTING.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions HELPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
| [RandomTrait](#randomtrait) | 10 | Replace random-value tokens in step arguments and table cells. |
| [RegionTrait](#regiontrait) | 4 | Interact with and assert against named page regions. |
| [ResponsiveTrait](#responsivetrait) | 7 | Test responsive layouts with viewport control. |
| [RestTrait](#resttrait) | 2 | Test REST APIs with lightweight steps and no Drupal dependencies. |
| [RestTrait](#resttrait) | 2 | Test REST APIs with lightweight steps. |
| [TableTrait](#tabletrait) | 10 | Interact with HTML table elements and assert their content. |
| [WaitTrait](#waittrait) | 2 | Wait for a period of time or for AJAX to finish. |
| [XmlTrait](#xmltrait) | 9 | Assert XML responses with element and attribute checks. |
Expand Down Expand Up @@ -578,7 +578,7 @@ Whether to scroll elements to the center of the viewport
<br/><br/>

```
class FeatureContext extends DrupalContext {
class FeatureContext extends WebContext {
public function elementGetScrollIntoViewCenter(): bool {
return FALSE;
}
Expand Down Expand Up @@ -1452,7 +1452,7 @@ Set custom breakpoints

[Source](src/Steps/Web/RestTrait.php), [Steps](STEPS.md#resttrait)

> Test REST APIs with lightweight steps and no Drupal dependencies.
> Test REST APIs with lightweight steps.

<details>
<summary><code>public function restGetClient(): mixed</code></summary>
Expand Down
22 changes: 16 additions & 6 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,14 @@ The queue steps go through `QueueCapabilityInterface`, which the Drush backend s

Every file step goes through `FileCapabilityInterface`, which only the in-process backend provides. `the unmanaged file at the URI :uri should exist` and its negative read `is_file()` rather than `file_exists()`, so a directory at the URI no longer counts as a file.

### Modules

`the module :module is enabled`, `the module :module is disabled` and their table forms no longer rebuild every cache after the install or the uninstall. Drupal's module installer rebuilds the container and the router itself, so the steps call `moduleInstall()` or `moduleUninstall()` and nothing else, and under `@backend:drush` no `cache:rebuild` follows the `pm:install` or `pm:uninstall`.

### Redirects

The redirect steps go through `RedirectCapabilityInterface`, which only the in-process backend provides. A source path and a destination are stored as the module's own `Redirect::setSource()` and `Redirect::setRedirect()` store them, so `/old` and `old` name the same redirect, and `/about`, `about` and `internal:/about` the same destination, in the creation step and the assertions alike.

### Entity cleanup

3.14 tore a scenario's entities down in 2 hooks. The Drupal Extension's `RawDrupalContext::cleanEntities()` deleted the nodes, terms and languages its creation methods made, and this package's `entityCleanupAfterScenario()` deleted the entities its traits registered, leaving out the types in `ENTITY_CLEANUP_EXCLUDED_TYPES` so the 2 never deleted the same one. 4.0 has 1 hook, `EntityLifecycleTrait::entityLifecycleAfterScenario()`, with no second registry and no exclusion list:
Expand All @@ -1317,7 +1325,8 @@ Every file step goes through `FileCapabilityInterface`, which only the in-proces
- The other columns of `the following managed files exist:`, such as `status` or `uid`, go through the field handlers, as the columns of every other creation step do, where 3.14 set each value on the file as written. A `uid` cell may name the user, and a value a handler can't resolve, such as a user that doesn't exist, fails the step.
- `the response XML is loaded from the file :filename`, `the response JSON is loaded from the file :filename` and the 4 `the response should match the ... in the file :filename` steps fail for a path that leaves `files_path`, with `The fixture file "..." is outside the configured "files_path".`, where 3.14 read the file outside it.
- A state value holding a JSON object is stored as an array. `StateTrait` decoded JSON to a `stdClass` while `ConfigTrait` decoded it to an array, and both now share `stringNormalizeValue()`, so `the state :name has the value :value` with `{"a":1}` stores `['a' => 1]`.
- `the draggable views items ... are saved in the following order:` resolves every title before it writes a weight. It used to write each row's weight as it went, so a missing title left the order half-saved. A missing title now fails before anything is written.
- `the draggable views items ... are saved in the following order:` resolves every title before it writes a weight. It used to write each row's weight as it went, so a missing title left the order half-saved. A missing title now fails before anything is written. A view that doesn't exist fails with `\RuntimeException` (`No view with the id "..." exists.`), where the weights used to be written under any name, and the saved order invalidates the list tags of the view's base entity type and the view's and the display's own tags, as the module's own form does, where `node_list` alone was invalidated.
- A textual `created`, `changed`, `revision_timestamp` or `timestamp` value, such as `-1 hour` or `2014-10-17 8:00am`, is read by the field handlers, so it works on every entity type with such a field where 3.14 converted it for a node alone. A value `strtotime()` can't read fails with `Timestamp field value "..." is not parseable.`

### Prerequisites

Expand Down Expand Up @@ -1749,7 +1758,7 @@ $value = $this->stateFindValue('my_module.launched');
| `ParametersTrait::getDrupalSelector()` | `ParametersTrait::getSelector()` | |
| `ParametersTrait::getDrupalText()` | `ParametersTrait::getText()` | |
| `ParametersTrait::getMapping()` | `MappingTrait::mappingGetValue()` | Compose `MappingTrait`, or extend `WebContext` or `DrupalContext`. A context that only uses the `{{ Key }}` token changes nothing |
| `RawDrupalContext::alterNodeParameters()` | `EntityLifecycleTrait::entityLifecycleBeforeNodeCreate()` | |
| `RawDrupalContext::alterNodeParameters()` | Removed | The backend's `TimestampHandler` reads a textual `created`, `changed` or `revision_timestamp` value, so nothing alters the node parameters before creation; see [Fields, files and stored values](#fields-files-and-stored-values) |
| `RawDrupalContext::cleanEntities()` | `EntityLifecycleTrait::entityLifecycleAfterScenario()` | Hook; see [Entity cleanup](#entity-cleanup) |
| `RawDrupalContext::cleanRoles()` | `AuthTrait::authCleanRoles()` | Protected, takes no parameters and isn't a hook, because `authAfterScenario()` is. An override drops its `@AfterScenario` annotation or `#[AfterScenario]` attribute, or it runs twice |
| `RawDrupalContext::cleanUsers()` | `AuthTrait::authCleanUsers()` | Protected, takes no parameters and isn't a hook, because `authAfterScenario()` is. An override drops its `@AfterScenario` annotation or `#[AfterScenario]` attribute, or it runs twice |
Expand Down Expand Up @@ -2240,7 +2249,7 @@ $role = $backend->createRole(['access content'])->getValue('id');

A create flags the stub as saved only when the backend holds the created entity. The Drush backend runs outside the test process, so its `createUser()` writes the new `uid` onto the stub and its `createRole()` stub carries the role's `id`, but neither stub is saved. Read the ID from the stub's values rather than from `getSavedEntity()` when the backend may be Drush.

In 3.14, the config, module, state and time steps called `\Drupal::configFactory()`, `\Drupal::moduleHandler()` and `\Drupal::state()` directly, so they ran only where the Drupal API driver had bootstrapped Drupal in the Behat process. They name the capability they need now, and run on any backend that provides it, Drush included. The lookups behind the delete, visit and assert steps ran `\Drupal::entityQuery()` the same way; they go through `QueryTrait::queryEntityIds()`, which resolves `EntityLookupCapabilityInterface`, and a delete step hands the id it found to the capability that deletes that entity type. The steps that changed an entity loaded it and saved it themselves; they hand a stub to `updateEntity()` now, and the menu, webform and block steps create, copy and change config entities through `ConfigEntityCapabilityInterface`. The queue, log, cache bin, file, search index, node access, workflow, block, path alias, menu link, entity URL, one-time login, user role, password and user activity operations, and the reads of entity type and field definitions, each sit on a capability the same way, and the config override header reaches a backend through one, so the 3 raw database queries the cache, draggable views and watchdog steps ran live in the in-process backend. A backend of your own that implements these capabilities adds the methods below; a project that uses only the shipped backends changes nothing.
In 3.14, the config, module, state and time steps called `\Drupal::configFactory()`, `\Drupal::moduleHandler()` and `\Drupal::state()` directly, so they ran only where the Drupal API driver had bootstrapped Drupal in the Behat process. They name the capability they need now, and run on any backend that provides it, Drush included. The lookups behind the delete, visit and assert steps ran `\Drupal::entityQuery()` the same way; they go through `QueryTrait::queryEntityIds()`, which resolves `EntityLookupCapabilityInterface`, and a delete step hands the id it found to the capability that deletes that entity type. The steps that changed an entity loaded it and saved it themselves; they hand a stub to `updateEntity()` now, and the menu, webform and block steps create, copy and change config entities through `ConfigEntityCapabilityInterface`. The queue, log, cache bin, file, search index, node access, workflow, moderation state, block, path alias, menu link, redirect, entity URL, one-time login, user role, password and user activity operations, and the reads of entity type, bundle and field definitions, each sit on a capability the same way, and the config override header reaches a backend through one, so the 3 raw database queries the cache, draggable views and watchdog steps ran live in the in-process backend. A backend of your own that implements these capabilities adds the methods below; a project that uses only the shipped backends changes nothing.

| Interface | Methods added in 4.0 |
| --- | --- |
Expand All @@ -2249,8 +2258,8 @@ In 3.14, the config, module, state and time steps called `\Drupal::configFactory
| `ConfigCapabilityInterface` | `configExists()`, `configGetData()`, `configSetData()`, `configDelete()` |
| `ConfigEntityCapabilityInterface` | `createConfigEntity()`, `updateConfigEntity()`, `deleteConfigEntity()`, `duplicateConfigEntity()`; the interface is new. Each saves through the entity storage, so the save hooks run, and an update or a copy loads the entity without overrides, so no override is written into the stored configuration. A create, an update and a copy return a stub, and a delete returns `void`. The Drush backend doesn't provide it |
| `ContentCapabilityInterface` | `updateEntity()`, `getEntityValues()`, `getEntityBundle()`, `getEntityUrl()`. An update sets every value of the stub through the field handlers, as a create does, and returns the stub; a read returns each field in its storage shape, or an empty array for an entity that doesn't exist. The in-process backend's `getEntityUrl()` returns the route's own path, without a path alias or the base path of a site in a subdirectory |
| `DraggableviewsCapabilityInterface` | `draggableviewsSetOrder()`; the interface is new. The Drush backend doesn't provide it |
| `EntityDefinitionCapabilityInterface` | `getEntityIdKey()`, `getEntityFieldTypes()`, `parseEntityFields()`; the interface is new. `getEntityFieldTypes()` moved here from `CoreInterface`, which extends the interface, so `Core` keeps the method. The Drush backend doesn't provide it |
| `DraggableviewsCapabilityInterface` | `draggableviewsSetOrder()`; the interface is new. It invalidates what the module's own form invalidates, the list tags of the view's base entity type and the view's and the display's own tags, and rejects a view that doesn't exist. The Drush backend doesn't provide it |
| `EntityDefinitionCapabilityInterface` | `getEntityIdKey()`, `getEntityFieldTypes()`, `parseEntityFields()`, `bundleExists()`, `getBundleLabel()`; the interface is new. `getEntityFieldTypes()` moved here from `CoreInterface`, which extends the interface, so `Core` keeps the method. The Drush backend doesn't provide it |
| `EntityLookupCapabilityInterface` | `findEntityIds()`; the interface is new. A condition is a value, matched with `=`, or an `[operator, value]` pair over `=`, `IN`, `CONTAINS` and `LIKE`, and the ids come back in natural order. The Drush backend doesn't provide it |
| `FileCapabilityInterface` | `fileWriteUnmanaged()`, `fileReadUnmanaged()`, `fileExistsUnmanaged()`, `fileDeleteUnmanaged()`, `filePrepareDirectory()`, `fileWriteManaged()`; the interface is new. The Drush backend doesn't provide it |
| `LogCapabilityInterface` | `logGetLastId()`, `logRead()`, `logDelete()`; the interface is new, and its `SEVERITY_*` constants hold the RFC 5424 levels. The Drush backend serves it with `watchdog:show` and `watchdog:delete`, and returns each message rendered with its placeholders and no `variables` |
Expand All @@ -2260,13 +2269,14 @@ In 3.14, the config, module, state and time steps called `\Drupal::configFactory
| `PathAliasCapabilityInterface` | `setEntityPathAlias()`; the interface is new. It changes the alias an entity has rather than adding a second one, and tells pathauto to leave the new one alone. The Drush backend doesn't provide it |
| `QueueCapabilityInterface` | `queueCountItems()`, `queueDelete()`, `queueProcess()`; the interface is new. The Drush backend serves it with `queue:list`, `queue:run` and `queue:delete` |
| `QueueItemCapabilityInterface` | `queueAddItem()`; the interface is new. The Drush backend doesn't provide it, because no Drush command adds an item |
| `RedirectCapabilityInterface` | `createRedirect()`, `deleteRedirect()`, `findRedirectIds()`; the interface is new. A source path and a destination are stored as `Redirect::setSource()` and `Redirect::setRedirect()` store them, so a caller passes them as a scenario writes them, with or without the leading slash and the `internal:` scheme. The Drush backend doesn't provide it |
| `RequestEnvironmentCapabilityInterface` | `requestHeaderSet()`, `requestHeaderUnset()`; the interface is new, under `DrevOps\BehatSteps\Backend\Capability`. The in-process backend writes the header to `$_SERVER` and the Drush backend passes it to each command in its environment, both under the name PHP gives a request header, such as `HTTP_X_CONFIG_NO_OVERRIDE`, and neither needs to be bootstrapped first |
| `SearchApiCapabilityInterface` | `searchIndexEntity()`, `searchRunCron()`; the interface is new. The Drush backend doesn't provide it |
| `SearchIndexCapabilityInterface` | `searchIndexItems()`; the interface is new. The Drush backend serves it with `search-api:index` |
| `StateCapabilityInterface` | `stateGet()`, `stateSet()`, `stateDelete()`, `stateExists()`; the interface is new |
| `UserActivityCapabilityInterface` | `setUserLastAccessTime()`, `setUserLastLoginTime()`; the interface is new. The Drush backend doesn't provide it |
| `UserCapabilityInterface` | `getOneTimeLoginUrl()`, `getUserRoles()`, `setUserPassword()`. The Drush backend serves them with `user:login`, `user:information` and `user:password` |
| `WorkflowCapabilityInterface` | `hasWorkflowState()`; the interface is new. The Drush backend doesn't provide it |
| `WorkflowCapabilityInterface` | `hasWorkflowState()`, `setModerationState()`; the interface is new. The Drush backend doesn't provide it |

`CacheCapabilityInterface::cacheClear()` took a cache bin to clear, which no backend honored. It takes no argument and clears every cache, so a call that passed a type drops it:

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Fetch https://raw.githubusercontent.com/drevops/behat-steps/main/.claude/skills/
| [RegionTrait](STEPS.md#regiontrait) | Interact with and assert against named page regions. |
| [ResponseTrait](STEPS.md#responsetrait) | Verify HTTP response headers. |
| [ResponsiveTrait](STEPS.md#responsivetrait) | Test responsive layouts with viewport control. |
| [RestTrait](STEPS.md#resttrait) | Test REST APIs with lightweight steps and no Drupal dependencies. |
| [RestTrait](STEPS.md#resttrait) | Test REST APIs with lightweight steps. |
| [TableTrait](STEPS.md#tabletrait) | Interact with HTML table elements and assert their content. |
| [WaitTrait](STEPS.md#waittrait) | Wait for a period of time or for AJAX to finish. |
| [XmlTrait](STEPS.md#xmltrait) | Assert XML responses with element and attribute checks. |
Expand Down
17 changes: 9 additions & 8 deletions STEPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
| [RegionTrait](#regiontrait) | Interact with and assert against named page regions. |
| [ResponseTrait](#responsetrait) | Verify HTTP response headers. |
| [ResponsiveTrait](#responsivetrait) | Test responsive layouts with viewport control. |
| [RestTrait](#resttrait) | Test REST APIs with lightweight steps and no Drupal dependencies. |
| [RestTrait](#resttrait) | Test REST APIs with lightweight steps. |
| [TableTrait](#tabletrait) | Interact with HTML table elements and assert their content. |
| [WaitTrait](#waittrait) | Wait for a period of time or for AJAX to finish. |
| [XmlTrait](#xmltrait) | Assert XML responses with element and attribute checks. |
Expand Down Expand Up @@ -3675,7 +3675,7 @@ Then the response header "Connection" should not contain "Keep-Alive"
> <br/><br/>
> Custom breakpoints:
> ```
> class FeatureContext extends DrupalContext {
> class FeatureContext extends WebContext {
> use ResponsiveTrait;
>
> #[BeforeScenario]
Expand Down Expand Up @@ -3770,7 +3770,7 @@ When I set the viewport to "375" by "667"

[Source](src/Steps/Web/RestTrait.php), [Example](tests/behat/features/rest.feature)

> Test REST APIs with lightweight steps and no Drupal dependencies.
> Test REST APIs with lightweight steps.
> - Set HTTP headers for subsequent requests.
> - Send requests with any HTTP method (GET, POST, PUT, PATCH, DELETE).
> - Assert response status codes and body content.
Expand Down Expand Up @@ -4098,7 +4098,8 @@ Then I should not see the link "Delete" in the row containing "Article title"
> <br/><br/>
> A wait on `jQuery.active` alone misses `Drupal.ajax` updates, so an
> assertion following a click can read the page before the update applies.
> The wait here watches both.
> The wait here watches both, and a page without `Drupal` takes the jQuery
> check alone.
> <br/><br/>
> Skip the automatic waits with tag: `@behat-steps-skip:WaitTrait`.

Expand Down Expand Up @@ -6993,7 +6994,7 @@ Then the queue "myqueue" should be empty

| Prerequisite | Capability |
| --- | --- |
| A backend in the scenario's list provides "ContentCapabilityInterface" | `ContentCapabilityInterface` |
| A backend in the scenario's list provides "RedirectCapabilityInterface" | `RedirectCapabilityInterface` |
| The "redirect" module from the "drupal/redirect" package is enabled | `ModuleCapabilityInterface` |

<details>
Expand Down Expand Up @@ -7144,9 +7145,9 @@ When the Search API Solr cron runs

> Manage and assert Drupal State API values with automatic revert.
> <br/><br/>
> Provides set, delete, and assertion steps for keys stored through
> `\Drupal::state()`. Touched keys are snapshotted on first access and
> reverted after the scenario finishes.
> Provides set, delete, and assertion steps for keys in the site's state
> key-value store. Touched keys are snapshotted on first access and reverted
> after the scenario finishes.
> <br/><br/>
> Skip the revert with `@behat-steps-skip:StateTrait`. The snapshot registry
> is cleared unconditionally before and after the scenario, so no snapshot
Expand Down
Loading
Loading