diff --git a/.gitignore b/.gitignore index 1b10983..90b961a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ +.lando.yml composer.lock vendor/ core/ \ No newline at end of file diff --git a/README.md b/README.md index a706d18..9d3419f 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,13 @@ Provides drush (`runDrush`) command. ### Configuration - working_directory: Working directory where drush should be executed. Defaults to codeception root. +- timeout: Timeout in seconds for drush command. Set to 0 for no timeout. Default to 60 seconds. - drush: Drush executable. Defaults to `drush`. ``` modules: - DrupalDrush: working_directory: './web' + timeout: 120 drush: './vendor/bin/drush' options: uri: http://mydomain.com @@ -60,9 +62,17 @@ modules: ### Usage -Run drush config import and store output. +Run drush status and store output. -`$output = $i->runDrush('cim -y');` +`$output = $i->runDrush('status');` + +Run drush config import and store output from STDERR. + +`$output = $i->runDrush('cim -y', [], TRUE)->getErrorOutput();` + +Run drush cache rebuild and store the exit code. + +`$exit_code = $i->runDrush('cr', [], TRUE)->getExitCode();` Get one-time login url. @@ -196,8 +206,16 @@ modules: // Fill title. $i->fillTextField(FormField::title(), 'Mans nosukums'); -// Select english language for content. +// Select option from select list by key or value. +// For custom fields, target (last parameter) usually needs to be set to an +// empty string. $i->selectOptionFromList(FormField::langcode(), 'en'); +$i->selectOptionFromList(FormField::langcode(), 'English'); +$i->selectOptionFromList(FormField::field_my_list(), 'Apple', ''); + +// Select the nth option from a select list. +$i->selectOptionFromList(FormField::langcode()); +$i->selectNthOptionFromList(MTOFormField::field_my_list(), 2, ''); // Fill first paragraph of type text. $page_elements = ParagraphFormField::field_page_elements(); diff --git a/composer.json b/composer.json index 001a5cc..ccdceea 100644 --- a/composer.json +++ b/composer.json @@ -9,14 +9,16 @@ } ], "require": { - "codeception/codeception": "^4.0", - "fzaninotto/faker": "^1.8", - "codeception/module-webdriver": "^1.1", - "webflo/drupal-finder": "^1.2" + "codeception/codeception": "^5", + "codeception/module-webdriver": "^3 || ^4", + "webflo/drupal-finder": "^1.2", + "fakerphp/faker": "^1.23", + "codeception/module-phpbrowser": "^3", + "codeception/module-asserts": "^3" }, "require-dev": { - "composer/installers": "^1", - "drupal/core": "^8" + "composer/installers": "^2", + "drupal/core": "^9 || ^10" }, "license": "GPL-2.0", "authors": [ @@ -30,5 +32,10 @@ "psr-4": { "Codeception\\": "src/Codeception" } + }, + "config": { + "allow-plugins": { + "composer/installers": true + } } } diff --git a/src/Codeception/Module/DrupalAcceptance.php b/src/Codeception/Module/DrupalAcceptance.php index b7bd495..c163e87 100644 --- a/src/Codeception/Module/DrupalAcceptance.php +++ b/src/Codeception/Module/DrupalAcceptance.php @@ -124,6 +124,24 @@ public function selectOptionFromList(IdentifiableFormFieldInterface $field, $opt $this->webdriver->selectOption($field->$target, $option); } + /** + * Select nth option from select list. + * + * Useful if you don't know what the options in the list are, and just want + * to select the first one, second one etc. + * + * @param \Codeception\Util\IdentifiableFormFieldInterface $field + * Select list form field. + * @param int $nth + * Nth option to get. Default to first option. + * @param string $target + * Target field. + */ + public function selectNthOptionFromList(IdentifiableFormFieldInterface $field, $nth = 1, $target = 'value') { + $option = $this->webdriver->grabTextFrom($field->{$target} . '/option[' . $nth . ']'); + $this->selectOptionFromList($field, $option, $target); + } + /** * Click on element. * @@ -158,6 +176,12 @@ public function addReferenceFieldItem(FormField $field) { public function fillWysiwygEditor(IdentifiableFormFieldInterface $field, $content) { $selector = $this->webdriver->grabAttributeFrom($field->value, 'id'); $script = "jQuery(function(){CKEDITOR.instances[\"$selector\"].setData(\"$content\")});"; + // Check if CKEditor5 is in use. If so, override the script as global + // registry of editor instances is no longer available and a different + // approach is used to access them via DOM. + if (!empty($this->webdriver->grabAttributeFrom($field->value, 'data-ckeditor5-id'))) { + $script = "document.querySelector(\"#$selector\").nextSibling.querySelector(\".ck-editor__editable_inline\").ckeditorInstance.setData(\"$content\")"; + } $this->webdriver->executeInSelenium(function (RemoteWebDriver $webDriver) use ($script) { $webDriver->executeScript($script); }); diff --git a/src/Codeception/Module/DrupalBootstrap.php b/src/Codeception/Module/DrupalBootstrap.php index 8a93e05..f4f2a39 100644 --- a/src/Codeception/Module/DrupalBootstrap.php +++ b/src/Codeception/Module/DrupalBootstrap.php @@ -17,9 +17,9 @@ * #### Example (DrupalBootstrap) * modules: * - DrupalBootstrap: - * root: './web' - * site_path: 'sites/default' - * http_host: 'mysite.local' + * root: './web' + * site_path: 'sites/default' + * http_host: 'mysite.local' * * @package Codeception\Module */ @@ -30,7 +30,7 @@ class DrupalBootstrap extends Module { * * @var array */ - protected $config = [ + protected array $config = [ 'site_path' => 'sites/default', ]; diff --git a/src/Codeception/Module/DrupalDrush.php b/src/Codeception/Module/DrupalDrush.php index 046e3b2..574b345 100644 --- a/src/Codeception/Module/DrupalDrush.php +++ b/src/Codeception/Module/DrupalDrush.php @@ -12,12 +12,13 @@ * #### Example (DrupalDrush) * modules: * - DrupalDrush: - * working_directory: './web' - * drush: './vendor/bin/drush' - * alias: '@mysite.com' - * options: - * uri: http://mydomain.com - * root: /app/web + * working_directory: './web' + * timeout: 120 + * drush: './vendor/bin/drush' + * alias: '@mysite.com' + * options: + * uri: http://mydomain.com + * root: /app/web * * @package Codeception\Module */ @@ -28,7 +29,7 @@ class DrupalDrush extends Module { * * @var array */ - protected $config = [ + protected array $config = [ 'drush' => 'drush', 'alias' => '', 'options' => [], @@ -42,11 +43,14 @@ class DrupalDrush extends Module { * e.g. "en devel -y". * @param array $options * Associative array of options. + * @param bool $return_process + * If TRUE, the Process object will be returned. If false, the output of + * Process::getOutput() will be returned. Defaults to FALSE. * - * @return string - * The process output. + * @return string|\Symfony\Component\Process\Process + * The process output, or the process itself. */ - public function runDrush($command, array $options = []) { + public function runDrush($command, array $options = [], $return_process = FALSE) { if ($alias = $this->_getConfig('alias')) { $command = $alias . ' ' . $command; } @@ -56,7 +60,7 @@ public function runDrush($command, array $options = []) { elseif ($this->_getConfig('options')) { $command = $this->normalizeOptions($this->_getConfig('options')) . $command; } - return Drush::runDrush($command, $this->_getConfig('drush'), $this->_getConfig('working_directory')); + return Drush::runDrush($command, $this->_getConfig('drush'), $this->_getConfig('working_directory'), $this->_getConfig('timeout'), $return_process); } /** diff --git a/src/Codeception/Module/DrupalEntity.php b/src/Codeception/Module/DrupalEntity.php index 0d1b84a..e04f99c 100644 --- a/src/Codeception/Module/DrupalEntity.php +++ b/src/Codeception/Module/DrupalEntity.php @@ -3,9 +3,10 @@ namespace Codeception\Module; use Codeception\Module; +use Codeception\TestInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Url; -use Codeception\TestCase; +use Exception; /** * Class DrupalEntity. @@ -14,12 +15,12 @@ * #### Example (DrupalEntity) * modules: * - DrupalEntity: - * cleanup_test: true - * cleanup_failed: false - * cleanup_suite: true - * route_entities: - * - node - * - taxonomy_term. + * cleanup_test: true + * cleanup_failed: false + * cleanup_suite: true + * route_entities: + * - node + * - taxonomy_term. * * @package Codeception\Module */ @@ -30,7 +31,7 @@ class DrupalEntity extends Module { * * @var array */ - protected $config = [ + protected array $config = [ 'cleanup_test' => TRUE, 'cleanup_failed' => TRUE, 'cleanup_suite' => TRUE, @@ -60,7 +61,7 @@ public function _afterSuite() { // @codingStandardsIgnoreLine /** * {@inheritdoc} */ - public function _after(TestCase $test) { // @codingStandardsIgnoreLine + public function _after(TestInterface $test) { // @codingStandardsIgnoreLine if ($this->config['cleanup_test']) { $this->doEntityCleanup(); } @@ -69,7 +70,7 @@ public function _after(TestCase $test) { // @codingStandardsIgnoreLine /** * {@inheritdoc} */ - public function _failed(TestCase $test, $fail) { // @codingStandardsIgnoreLine + public function _failed(TestInterface $test, Exception $fail) { // @codingStandardsIgnoreLine if ($this->config['cleanup_failed']) { $this->doEntityCleanup(); } diff --git a/src/Codeception/Module/DrupalUser.php b/src/Codeception/Module/DrupalUser.php index 8f6112a..46da663 100644 --- a/src/Codeception/Module/DrupalUser.php +++ b/src/Codeception/Module/DrupalUser.php @@ -3,11 +3,12 @@ namespace Codeception\Module; use Codeception\Module; -use Drupal\Core\Config\StorageInterface; +use Codeception\TestInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\user\Entity\User; use Faker\Factory; use Codeception\Util\Drush; +use Exception; /** * Class DrupalUser. @@ -16,17 +17,17 @@ * #### Example (DrupalUser) * modules: * - DrupalUser: - * default_role: 'authenticated' - * driver: 'PhpBrowser' - * drush: './vendor/bin/drush' - * cleanup_entities: - * - media - * - file - * - paragraph - * cleanup_test: false - * cleanup_failed: false - * cleanup_suite: true - * alias: @site.com + * default_role: 'authenticated' + * driver: 'PhpBrowser' + * drush: './vendor/bin/drush' + * cleanup_entities: + * - media + * - file + * - paragraph + * cleanup_test: false + * cleanup_failed: false + * cleanup_suite: true + * alias: @site.com * * @package Codeception\Module */ @@ -51,7 +52,7 @@ class DrupalUser extends Module { * * @var array */ - protected $config = [ + protected array $config = [ 'alias' => '', 'default_role' => 'authenticated', 'driver' => 'WebDriver', @@ -65,7 +66,7 @@ class DrupalUser extends Module { /** * {@inheritdoc} */ - public function _beforeSuite($settings = []) { // @codingStandardsIgnoreLine + public function _beforeSuite(array $settings = []) { // @codingStandardsIgnoreLine $this->driver = null; if (!$this->hasModule($this->_getConfig('driver'))) { $this->fail('User driver module not found.'); @@ -77,7 +78,7 @@ public function _beforeSuite($settings = []) { // @codingStandardsIgnoreLine /** * {@inheritdoc} */ - public function _after(\Codeception\TestCase $test) { // @codingStandardsIgnoreLine + public function _after(TestInterface $test) { // @codingStandardsIgnoreLine if ($this->_getConfig('cleanup_test')) { $this->userCleanup(); } @@ -86,7 +87,7 @@ public function _after(\Codeception\TestCase $test) { // @codingStandardsIgnoreL /** * {@inheritdoc} */ - public function _failed(\Codeception\TestCase $test, $fail) { // @codingStandardsIgnoreLine + public function _failed(TestInterface $test, Exception $fail) { // @codingStandardsIgnoreLine if ($this->_getConfig('cleanup_failed')) { $this->userCleanup(); } @@ -143,7 +144,7 @@ public function createUserWithRoles(array $roles = [], $password = FALSE) { */ public function logInAs($username) { $alias = $this->_getConfig('alias') ? $this->_getConfig('alias') . ' ' : ''; - $output = Drush::runDrush($alias. 'uli --name=' . $username, $this->_getConfig('drush'), $this->_getConfig('working_directory')); + $output = Drush::runDrush($alias. 'uli --name=' . $username, $this->_getConfig('drush'), $this->_getConfig('working_directory'), $this->_getConfig('timeout')); $gen_url = str_replace(PHP_EOL, '', $output); $url = substr($gen_url, strpos($gen_url, '/user/reset')); $this->driver->amOnPage($url); diff --git a/src/Codeception/Module/DrupalWatchdog.php b/src/Codeception/Module/DrupalWatchdog.php index 2398476..19273c8 100644 --- a/src/Codeception/Module/DrupalWatchdog.php +++ b/src/Codeception/Module/DrupalWatchdog.php @@ -35,7 +35,7 @@ class DrupalWatchdog extends Module { * * @var array */ - protected $config = [ + protected array $config = [ 'channels' => [], 'level' => 'ERROR', 'enabled' => TRUE, diff --git a/src/Codeception/TestDrupalKernel.php b/src/Codeception/TestDrupalKernel.php index 4ee4164..8e16f92 100644 --- a/src/Codeception/TestDrupalKernel.php +++ b/src/Codeception/TestDrupalKernel.php @@ -4,11 +4,11 @@ use Drupal\Core\DrupalKernel; use Drupal\Core\Site\Settings; -use Symfony\Cmf\Component\Routing\RouteObjectInterface; +use Drupal\Core\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; -class TestDrupalKernel extends DrupalKernel{ +class TestDrupalKernel extends DrupalKernel { /** * TestDrupalKernel constructor. @@ -44,4 +44,4 @@ public function bootTestEnvironment($sitePath, Request $request){ $this->container->get('router.request_context')->fromRequest($request); } -} \ No newline at end of file +} diff --git a/src/Codeception/Util/Drupal/FormField.php b/src/Codeception/Util/Drupal/FormField.php index 68945e5..5fd6411 100644 --- a/src/Codeception/Util/Drupal/FormField.php +++ b/src/Codeception/Util/Drupal/FormField.php @@ -90,15 +90,16 @@ public function __toString() { /** * Returns xpath of current identifiers element. * - * @param string $name + * @param string $element * Name of element. * * @return string * Returns path with current identifier plus requested subfield. */ - public function __get($name) { + public function __get($element = '') { + $suffix = $element ? '-' . $this->normalise($element) : ''; return $this->getXpath([ - 'identifier' => $this->getCurrentIdentifier() . '-' . $this->normalise($name), + 'identifier' => $this->getCurrentIdentifier() . $suffix, ]); } @@ -142,7 +143,7 @@ public function getCurrentIdentifier() { * @param string $element * Name of element. * - * @return mixed + * @return string * Returns path with identifier plus requested subfield. */ public function get($element = '') { diff --git a/src/Codeception/Util/Drupal/MTOFormField.php b/src/Codeception/Util/Drupal/MTOFormField.php index 4758e71..b6abde4 100644 --- a/src/Codeception/Util/Drupal/MTOFormField.php +++ b/src/Codeception/Util/Drupal/MTOFormField.php @@ -80,16 +80,14 @@ public function __toString() { /** * Returns xpath of current identifiers element. * - * @param string $name + * @param string $element * Name of element. * * @return string * Returns path with current identifier plus requested subfield. */ - public function __get($name) { - return $this->getXpath([ - 'identifier' => $this->getIdentifier() . '-' . $this->normalise($name), - ]); + public function __get($element = '') { + return $this->get($element); } /** @@ -132,7 +130,7 @@ public function getCurrentIdentifier() { * @param string $element * Name of element. * - * @return mixed + * @return string * Returns path with identifier plus requested subfield. */ public function get($element = '') { diff --git a/src/Codeception/Util/Drush.php b/src/Codeception/Util/Drush.php index 9ee75cc..cb2d01b 100644 --- a/src/Codeception/Util/Drush.php +++ b/src/Codeception/Util/Drush.php @@ -21,11 +21,16 @@ class Drush { * The drush command to use. * @param string $pwd * Working directory. + * @param int|float $timeout. + * Drush execution timeout. + * @param bool $return_process + * If TRUE, the Process object will be returned. If false, the output of + * Process::getOutput() will be returned. Defaults to FALSE. * - * @return string - * The process output. + * @return string|\Symfony\Component\Process\Process + * The process output, or the process object itself. */ - public static function runDrush($command, $drush = 'drush', $pwd = NULL) { + public static function runDrush($command, $drush = 'drush', $pwd = NULL, $timeout = NULL, $return_process = FALSE) { $command_args = array_merge([$drush], explode(' ', $command)); $process = new Process($command_args); @@ -34,7 +39,14 @@ public static function runDrush($command, $drush = 'drush', $pwd = NULL) { $process->setWorkingDirectory($pwd); } - return $process->mustRun()->getOutput(); + // Set timeout if configured. + if (isset($timeout)) { + $process->setTimeout($timeout); + } + + $process->mustRun(); + + return $return_process ? $process : $process->getOutput(); } }