Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add codeception checks for event triggers and login without a browser #23

Draft
wants to merge 12 commits into
base: 9.x
Choose a base branch
from
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