Skip to content
Draft
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea/
composer.lock
vendor/
core/
core/
modules/
18 changes: 13 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
}
],
"require": {
"codeception/codeception": "^4.0",
"fzaninotto/faker": "^1.8",
"codeception/module-webdriver": "^1.1",
"webflo/drupal-finder": "^1.2"
"codeception/codeception": "^4.0 || ^5.0",
"codeception/module-webdriver": "*",
"webflo/drupal-finder": "^1.2",
"drupal/devel": "^4"
},
"require-dev": {
"composer/installers": "^1",
"drupal/core": "^8"
"drupal/core": "^8 || ^9 || ^10"
},
"suggest" :{
"fzaninotto/faker": "^1.9"
},
"license": "GPL-2.0",
"authors": [
Expand All @@ -30,5 +33,10 @@
"psr-4": {
"Codeception\\": "src/Codeception"
}
},
"config": {
"allow-plugins": {
"composer/installers": true
}
}
}
32 changes: 31 additions & 1 deletion src/Codeception/Module/DrupalBootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Codeception\TestDrupalKernel;
use Symfony\Component\HttpFoundation\Request;
use DrupalFinder\DrupalFinder;

use Codeception\Module\DrupalBootstrap\EventsAssertionsTrait;

/**
* Class DrupalBootstrap.
Expand All @@ -25,6 +25,8 @@
*/
class DrupalBootstrap extends Module {

use EventsAssertionsTrait;

/**
* Default module configuration.
*
Expand All @@ -34,6 +36,13 @@ class DrupalBootstrap extends Module {
'site_path' => 'sites/default',
];

/**
* Track wether we enabled the webprofiler module or not.
*
* @var bool
*/
protected $enabledWebProfiler = FALSE;

/**
* DrupalBootstrap constructor.
*
Expand Down Expand Up @@ -72,4 +81,25 @@ public function __construct(ModuleContainer $container, $config = NULL) {
$kernel->bootTestEnvironment($this->_getConfig('site_path'), $request);
}

/**
* Enabled dependent modules.
*/
public function _beforeSuite($settings = []) {
$module_handler = \Drupal::service('module_handler');
if (!$module_handler->moduleExists('webprofiler')) {
$this->enabledWebProfiler = TRUE;
\Drupal::service('module_installer')->install(['webprofiler']);
}
}

/**
* Disable modules which were enabled.
*/
public function _afterSuite($settings = []) {
if ($this->enabledWebProfiler) {
$this->enabledWebProfiler = FALSE;
\Drupal::service('module_installer')->uninstall(['webprofiler']);
}
}

}
183 changes: 183 additions & 0 deletions src/Codeception/Module/DrupalBootstrap/EventsAssertionsTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

declare(strict_types=1);

namespace Codeception\Module\DrupalBootstrap;

use Drupal\webprofiler\DataCollector\EventsDataCollector;
use Drupal\webprofiler\EventDispatcher\EventDispatcherTraceableInterface;
use function get_class;
use function is_array;
use function is_object;

/**
*
*/
trait EventsAssertionsTrait {

/**
* Verifies that there were no orphan events during the test.
*
* An orphan event is an event that was triggered by manually executing the
* [`dispatch()`](https://symfony.com/doc/current/components/event_dispatcher.html#dispatch-the-event) method
* of the EventDispatcher but was not handled by any listener after it was dispatched.
*
* ```php
* <?php
* $I->dontSeeOrphanEvent();
* $I->dontSeeOrphanEvent('App\MyEvent');
* $I->dontSeeOrphanEvent(new App\Events\MyEvent());
* $I->dontSeeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']);
* ```
*
* @param string|object|string[] $expected
*/
public function dontSeeOrphanEvent($expected = NULL): void {
$eventCollector = $this->grabEventCollector();

$data = $eventCollector->getOrphanedEvents();
$expected = is_array($expected) ? $expected : [$expected];

if ($expected === NULL) {
$this->assertSame(0, count($data));
}
else {
$this->assertEventNotTriggered($data, $expected);
}
}

/**
* Verifies that one or more event listeners were not called during the test.
*
* ```php
* <?php
* $I->dontSeeEventTriggered('App\MyEvent');
* $I->dontSeeEventTriggered(new App\Events\MyEvent());
* $I->dontSeeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']);
* $I->dontSeeEventTriggered('my_event_string_name');
* $I->dontSeeEventTriggered(['my_event_string', 'my_other_event_string]);
* ```
*
* @param string|object|string[] $expected
*/
public function dontSeeEventTriggered($expected): void {
$eventCollector = $this->grabEventCollector();

$data = $eventCollector->getCalledListeners();
$expected = is_array($expected) ? $expected : [$expected];

$this->assertEventNotTriggered($data, $expected);
}

/**
* Verifies that one or more orphan events were dispatched during the test.
*
* An orphan event is an event that was triggered by manually executing the
* [`dispatch()`](https://symfony.com/doc/current/components/event_dispatcher.html#dispatch-the-event) method
* of the EventDispatcher but was not handled by any listener after it was dispatched.
*
* ```php
* <?php
* $I->seeOrphanEvent('App\MyEvent');
* $I->seeOrphanEvent(new App\Events\MyEvent());
* $I->seeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']);
* $I->seeOrphanEvent('my_event_string_name');
* $I->seeOrphanEvent(['my_event_string_name', 'my_other_event_string]);
* ```
*
* @param string|object|string[] $expected
*/
public function seeOrphanEvent($expected): void {
$eventCollector = $this->grabEventCollector();

$data = $eventCollector->getOrphanedEvents();
$expected = is_array($expected) ? $expected : [$expected];

$this->assertEventTriggered($data, $expected);
}

/**
* Verifies that one or more event listeners were called during the test.
*
* ```php
* <?php
* $I->seeEventTriggered('App\MyEvent');
* $I->seeEventTriggered(new App\Events\MyEvent());
* $I->seeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']);
* $I->seeEventTriggered('my_event_string_name');
* $I->seeEventTriggered(['my_event_string_name', 'my_other_event_string]);
* ```
*
* @param string|object|string[] $expected
*/
public function seeEventTriggered($expected): void {
$eventCollector = $this->grabEventCollector();

$data = $eventCollector->getCalledListeners();
$expected = is_array($expected) ? $expected : [$expected];

$this->assertEventTriggered($data, $expected);
}

/**
*
*/
protected function assertEventNotTriggered(array $data, array $expected): void {
foreach ($expected as $expectedEvent) {
$expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent;
$this->assertFalse(
$this->eventWasTriggered($data, (string) $expectedEvent),
"The '{$expectedEvent}' event triggered"
);
}
}

/**
*
*/
protected function assertEventTriggered(array $data, array $expected): void {
if (count($data) === 0) {
$this->fail('No event was triggered');
}

foreach ($expected as $expectedEvent) {
$expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent;
$this->assertTrue(
$this->eventWasTriggered($data, (string) $expectedEvent),
"The '{$expectedEvent}' event did not trigger"
);
}
}

/**
*
*/
protected function eventWasTriggered(array $actual, string $expectedEvent): bool {
$triggered = FALSE;

foreach ($actual as $name => $actualEvent) {
// Called Listeners.
if ($name === $expectedEvent && !empty($actualEvent)) {
$triggered = TRUE;
}
}

return $triggered;
}

/**
* Get the event data collector service.
*/
protected function grabEventCollector(): EventsDataCollector {
$event_dispatcher = \Drupal::service('event_dispatcher');
if ($event_dispatcher instanceof EventDispatcherTraceableInterface) {
$collector = new EventsDataCollector($event_dispatcher);
$collector->lateCollect();
return $collector;
}
else {
throw new \Exception('Webprofiler module is required for testing events.');
}
}

}
106 changes: 106 additions & 0 deletions src/Codeception/Module/DrupalGroup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace Codeception\Module;

use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\group\Entity\GroupInterface;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;

/**
* Class DrupalGroup.
*
* ### Example
* #### Example (DrupalGroup)
* modules:
* - DrupalGroup:
* cleanup_test: true
* cleanup_failed: false
* cleanup_suite: true
* route_entities:
* - node
* - taxonomy_term.
*
* @package Codeception\Module
*/
class DrupalGroup extends DrupalEntity {

/**
* Wrapper method to create a group.
*
* Improves readbility of tests by having the method read "create group".
*/
public function createGroup(array $values = [], $validate = TRUE) {
$type = 'group';

if (!array_key_exists('uid', $values)) {
$values['uid'] = 1;
}

try {
$entity = \Drupal::entityTypeManager()
->getStorage($type)
->create($values);
if ($validate && $entity instanceof FieldableEntityInterface) {
$violations = $entity->validate();
if ($violations->count() > 0) {
$message = PHP_EOL;
foreach ($violations as $violation) {
$message .= $violation->getPropertyPath() . ': ' . $violation->getMessage() . PHP_EOL;
}
throw new \Exception($message);
}
}
// Group specific entity save options.
$entity->setOwner(User::load($values['uid'] ?? 1));
$entity->save();
}
catch (\Exception $e) {
$this->fail('Could not create group entity. Error message: ' . $e->getMessage());
}
if (!empty($entity)) {
$this->registerTestEntity($entity->getEntityTypeId(), $entity->id());

return $entity;
}

return FALSE;
}

/**
* Join the defined group.
*
* @param \Drupal\group\Entity\GroupInterface $group
* Instance of a group.
* @param \Drupal\user\UserInterface $user
* A drupal user to add to the group.
*
* @return \Drupal\group\GroupMembership|false
* Returns the group content entity, FALSE otherwise.
*/
public function joinGroup(GroupInterface $group, UserInterface $user) {
$group->addMember($user);
return $group->getMember($user);
}

/**
* Leave a group.
*
* @param \Drupal\group\Entity\GroupInterface $group
* Instance of a group.
* @param \Drupal\user\UserInterface $user
* A drupal user to add to the group.
*
* @return bool
* Returns the TRUE if the user is no longer a member of the group,
* FALSE otherwise.
*/
public function leaveGroup(GroupInterface $group, UserInterface $user) {
$group->removeMember($user);
// Get member should return FALSE if the user isn't a member so we
// reverse the logic. If they are still a member it'll cast to TRUE.
$is_member = (bool) $group->getMember($user);
return !$is_member;
}

}
Loading