diff --git a/core/drupalci.yml b/core/drupalci.yml index 413272eff3f..46eeffa0228 100644 --- a/core/drupalci.yml +++ b/core/drupalci.yml @@ -16,34 +16,34 @@ build: # halt-on-fail can be set on the run_tests tasks in order to fail fast. # suppress-deprecations is false in order to be alerted to usages of # deprecated code. - run_tests.phpunit: - types: 'PHPUnit-Unit' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - run_tests.kernel: - types: 'PHPUnit-Kernel' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - run_tests.build: - # Limit concurrency due to disk space concerns. - concurrency: 15 - types: 'PHPUnit-Build' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - run_tests.functional: - types: 'PHPUnit-Functional' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false +# run_tests.phpunit: +# types: 'PHPUnit-Unit' +# testgroups: '--all' +# suppress-deprecations: false +# halt-on-fail: false +# run_tests.kernel: +# types: 'PHPUnit-Kernel' +# testgroups: '--all' +# suppress-deprecations: false +# halt-on-fail: false +# run_tests.build: +# # Limit concurrency due to disk space concerns. +# concurrency: 15 +# types: 'PHPUnit-Build' +# testgroups: '--all' +# suppress-deprecations: false +# halt-on-fail: false +# run_tests.functional: +# types: 'PHPUnit-Functional' +# testgroups: '--all' +# suppress-deprecations: false +# halt-on-fail: false run_tests.javascript: - concurrency: 15 + concurrency: 1 types: 'PHPUnit-FunctionalJavascript' testgroups: '--all' suppress-deprecations: false halt-on-fail: false - # Run nightwatch testing. - # @see https://www.drupal.org/project/drupal/issues/2869825 - nightwatchjs: {} +# # Run nightwatch testing. +# # @see https://www.drupal.org/project/drupal/issues/2869825 +# nightwatchjs: {} diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFormMessagesTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFormMessagesTest.php index 4fccd1c8427..82ed4024997 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFormMessagesTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFormMessagesTest.php @@ -2,10 +2,8 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; -use Behat\Mink\Element\NodeElement; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; -use WebDriver\Exception\UnknownError; /** * Tests that messages appear in the off-canvas dialog with configuring blocks. @@ -59,14 +57,14 @@ public function testValidationMessage() { // Enable layout builder. $this->drupalGet($field_ui_prefix . '/display/default'); $this->submitForm(['layout[enabled]' => TRUE], 'Save'); - $this->clickElementWhenClickable($page->findLink('Manage layout')); + $page->findLink('Manage layout')->click(); $assert_session->addressEquals($field_ui_prefix . '/display/default/layout'); - $this->clickElementWhenClickable($page->findLink('Add block')); + $page->findLink('Add block')->click(); $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas .block-categories')); - $this->clickElementWhenClickable($page->findLink('Powered by Drupal')); + $page->findLink('Powered by Drupal')->click(); $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas [name="settings[label]"]')); $page->findField('Title')->setValue(''); - $this->clickElementWhenClickable($page->findButton('Add block')); + $page->findButton('Add block')->click(); $this->assertMessagesDisplayed(); $page->findField('Title')->setValue('New title'); $page->pressButton('Add block'); @@ -76,7 +74,7 @@ public function testValidationMessage() { $assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas'); $assert_session->assertWaitOnAjaxRequest(); $this->drupalGet($this->getUrl()); - $this->clickElementWhenClickable($page->findButton('Save layout')); + $page->findButton('Save layout')->click(); $this->assertNotEmpty($assert_session->waitForElement('css', 'div:contains("The layout has been saved")')); // Ensure that message are displayed when configuring an existing block. @@ -85,7 +83,7 @@ public function testValidationMessage() { $this->clickContextualLink($block_css_locator, 'Configure', TRUE); $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas [name="settings[label]"]')); $page->findField('Title')->setValue(''); - $this->clickElementWhenClickable($page->findButton('Update')); + $page->findButton('Update')->click(); $this->assertMessagesDisplayed(); } @@ -106,34 +104,4 @@ protected function assertMessagesDisplayed() { $this->assertGreaterThan(4, count($top_form_elements)); } - /** - * Attempts to click an element until it is in a clickable state. - * - * @param \Behat\Mink\Element\NodeElement $element - * The element to click. - * @param int $timeout - * (Optional) Timeout in milliseconds, defaults to 10000. - * - * @todo Replace this method with general solution for random click() test - * failures in https://www.drupal.org/node/3032275. - */ - protected function clickElementWhenClickable(NodeElement $element, $timeout = 10000) { - $page = $this->getSession()->getPage(); - - $result = $page->waitFor($timeout / 1000, function () use ($element) { - try { - $element->click(); - return TRUE; - } - catch (UnknownError $exception) { - if (strstr($exception->getMessage(), 'not clickable') === FALSE) { - // Rethrow any unexpected UnknownError exceptions. - throw $exception; - } - return NULL; - } - }); - $this->assertTrue($result); - } - } diff --git a/core/modules/system/tests/modules/js_click_test/css/js-click-test-blocker-element.css b/core/modules/system/tests/modules/js_click_test/css/js-click-test-blocker-element.css new file mode 100644 index 00000000000..df065ee866b --- /dev/null +++ b/core/modules/system/tests/modules/js_click_test/css/js-click-test-blocker-element.css @@ -0,0 +1,13 @@ +.blocker-element { + /* Position the box over the target link. */ + position: relative; + z-index: 1; + top: -30px; + left: -5px; + /* Resize the box to cover the target link. */ + width: 100px; + height: 40px; + opacity: 0.5; + /* Make the blocker element visible. */ + background-color: black; +} diff --git a/core/modules/system/tests/modules/js_click_test/js/js_click_test.trigger_link.es6.js b/core/modules/system/tests/modules/js_click_test/js/js_click_test.trigger_link.es6.js new file mode 100644 index 00000000000..ea46eb20ab8 --- /dev/null +++ b/core/modules/system/tests/modules/js_click_test/js/js_click_test.trigger_link.es6.js @@ -0,0 +1,23 @@ +/** + * @file + * Testing behavior for JSClickTest. + */ + +(($, { behaviors }, { triggerLink }) => { + /** + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches the click listener on the trigger link. + */ + behaviors.js_click_test_trigger_link = { + attach() { + $(once('trigger-link', '.trigger-link')).on('click', (event) => { + event.preventDefault(); + setTimeout(() => { + $('.blocker-element').remove(); + }, triggerLink.milliseconds); + }); + }, + }; +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/system/tests/modules/js_click_test/js/js_click_test.trigger_link.js b/core/modules/system/tests/modules/js_click_test/js/js_click_test.trigger_link.js new file mode 100644 index 00000000000..7656aa19ca0 --- /dev/null +++ b/core/modules/system/tests/modules/js_click_test/js/js_click_test.trigger_link.js @@ -0,0 +1,21 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, _ref, _ref2) { + var behaviors = _ref.behaviors; + var triggerLink = _ref2.triggerLink; + behaviors.js_click_test_trigger_link = { + attach: function attach() { + $(once('trigger-link', '.trigger-link')).on('click', function (event) { + event.preventDefault(); + setTimeout(function () { + $('.blocker-element').remove(); + }, triggerLink.milliseconds); + }); + } + }; +})(jQuery, Drupal, drupalSettings); \ No newline at end of file diff --git a/core/modules/system/tests/modules/js_click_test/js_click_test.info.yml b/core/modules/system/tests/modules/js_click_test/js_click_test.info.yml new file mode 100644 index 00000000000..6d1375dea60 --- /dev/null +++ b/core/modules/system/tests/modules/js_click_test/js_click_test.info.yml @@ -0,0 +1,5 @@ +name: 'JS Click Test' +type: module +description: 'Module for testing fault-tolerant clicks in JavaScript tests.' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/js_click_test/js_click_test.libraries.yml b/core/modules/system/tests/modules/js_click_test/js_click_test.libraries.yml new file mode 100644 index 00000000000..105e32df9fb --- /dev/null +++ b/core/modules/system/tests/modules/js_click_test/js_click_test.libraries.yml @@ -0,0 +1,12 @@ +js_click_test: + version: VERSION + js: + js/js_click_test.trigger_link.js: {} + css: + theme: + css/js-click-test-blocker-element.css: {} + dependencies: + - core/drupal + - core/drupalSettings + - core/jquery + - core/once diff --git a/core/modules/system/tests/modules/js_click_test/js_click_test.routing.yml b/core/modules/system/tests/modules/js_click_test/js_click_test.routing.yml new file mode 100644 index 00000000000..de413b969ab --- /dev/null +++ b/core/modules/system/tests/modules/js_click_test/js_click_test.routing.yml @@ -0,0 +1,6 @@ +js_click_test.js_click_test: + path: '/js_click_test/{milliseconds}' + defaults: + _controller: '\Drupal\js_click_test\Controller\JSClickTestController::build' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/js_click_test/src/Controller/JSClickTestController.php b/core/modules/system/tests/modules/js_click_test/src/Controller/JSClickTestController.php new file mode 100644 index 00000000000..6d2f1b9f0d8 --- /dev/null +++ b/core/modules/system/tests/modules/js_click_test/src/Controller/JSClickTestController.php @@ -0,0 +1,60 @@ + [ + '#type' => 'link', + '#url' => Url::fromRoute(''), + '#title' => $this->t('Target link'), + ], + 'blocker_element' => [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => [ + 'class' => ['blocker-element'], + ], + ], + 'trigger_link' => [ + '#type' => 'link', + '#url' => Url::fromRoute(''), + '#title' => $this->t('Trigger link'), + '#attributes' => [ + 'class' => ['trigger-link'], + ], + ], + '#attached' => [ + 'library' => [ + 'js_click_test/js_click_test', + ], + 'drupalSettings' => [ + 'triggerLink' => [ + 'milliseconds' => $milliseconds, + ], + ], + ], + ]; + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/FindJavascriptElementsTrait.php b/core/tests/Drupal/FunctionalJavascriptTests/FindJavascriptElementsTrait.php new file mode 100644 index 00000000000..610af70330f --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/FindJavascriptElementsTrait.php @@ -0,0 +1,29 @@ +getXpath(), $this->elementSession); + } + return $elements; + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDocumentElement.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDocumentElement.php new file mode 100644 index 00000000000..fc1a96f9fc2 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDocumentElement.php @@ -0,0 +1,23 @@ +elementSession = $session; + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptNodeElement.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptNodeElement.php new file mode 100644 index 00000000000..b3f8968c453 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptNodeElement.php @@ -0,0 +1,75 @@ +elementSession = $session; + } + + /** + * {@inheritdoc} + */ + public function click() { + /** @var \WebDriver\Exception $not_clickable_exception */ + $not_clickable_exception = NULL; + $result = $this->waitFor(10, function () use (&$not_clickable_exception) { + try { + parent::click(); + return TRUE; + } + catch (Exception $exception) { + if (!preg_match('/not (clickable|interactable)/', $exception->getMessage())) { + // Rethrow any unexpected UnknownError exceptions. + throw $exception; + } + $not_clickable_exception = $exception; + return NULL; + } + }); + if ($result !== TRUE) { + throw $not_clickable_exception; + } + } + + /** + * {@inheritdoc} + */ + public function setValue($value) { + /** @var \WebDriver\Exception $not_clickable_exception */ + $not_clickable_exception = NULL; + $result = $this->waitFor(10, function () use (&$not_clickable_exception, $value) { + try { + parent::setValue($value); + return TRUE; + } + catch (Exception $exception) { + // cspell:ignore interactable + if (!preg_match('/not (clickable|interactable)/', $exception->getMessage())) { + // Rethrow any unexpected UnknownError exceptions. + throw $exception; + } + $not_clickable_exception = $exception; + return NULL; + } + }); + if ($result !== TRUE) { + throw $not_clickable_exception; + } + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptSession.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptSession.php new file mode 100644 index 00000000000..6c3797159a1 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptSession.php @@ -0,0 +1,39 @@ +page = new JavascriptDocumentElement($this); + } + + /** + * Returns the page. + * + * @return \Drupal\FunctionalJavascriptTests\JavascriptDocumentElement + * The page. + */ + public function getPage() { + return $this->page; + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Tests/ExtendedSessionTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Tests/ExtendedSessionTest.php new file mode 100644 index 00000000000..77441fe68ee --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Tests/ExtendedSessionTest.php @@ -0,0 +1,51 @@ +getSession(); + $this->assertInstanceOf(JavascriptSession::class, $session); + + $page = $session->getPage(); + $this->assertInstanceOf(JavascriptDocumentElement::class, $page); + + $body = $page->find('css', 'body'); + $this->assertInstanceOf(JavascriptNodeElement::class, $body); + + $div = $body->find('css', 'div'); + $this->assertInstanceOf(JavascriptNodeElement::class, $div); + + $divs = $page->findAll('css', 'div'); + $this->assertNotEmpty($divs); + foreach ($divs as $div) { + $this->assertInstanceOf(JavascriptNodeElement::class, $div); + } + + $divs = $body->findAll('css', 'div'); + $this->assertNotEmpty($divs); + foreach ($divs as $div) { + $this->assertInstanceOf(JavascriptNodeElement::class, $div); + } + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSClickTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSClickTest.php new file mode 100644 index 00000000000..2b721037094 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSClickTest.php @@ -0,0 +1,45 @@ +expectException(Exception::class); + $this->drupalGet('/js_click_test/100'); + $this->clickLink('Target link'); + } + + /** + * Assert no exception is thrown when blocker element is removed. + */ + public function testClickable() { + $this->drupalGet('/js_click_test/100'); + $this->clickLink('Trigger link'); + $this->clickLink('Target link'); + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/WebDriverTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/WebDriverTestBase.php index 20f04782735..17be0c9e113 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/WebDriverTestBase.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/WebDriverTestBase.php @@ -38,6 +38,11 @@ abstract class WebDriverTestBase extends BrowserTestBase { */ protected $minkDefaultDriverClass = DrupalSelenium2Driver::class; + /** + * {@inheritdoc} + */ + protected $sessionClassName = JavascriptSession::class; + /** * {@inheritdoc} */ diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index 9e5376eb4a5..1fc07492ed6 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -153,6 +153,15 @@ abstract class BrowserTestBase extends TestCase { */ protected $minkDefaultDriverClass = BrowserKitDriver::class; + /** + * Mink session class to use. + * + * Should be a fully-qualified class name. + * + * @var string + */ + protected $sessionClassName = Session::class; + /** * Mink default driver params. * @@ -255,7 +264,7 @@ protected function initMink() { $selectors_handler = new SelectorsHandler([ 'hidden_field_selector' => new HiddenFieldSelector(), ]); - $session = new Session($driver, $selectors_handler); + $session = new $this->sessionClassName($driver, $selectors_handler); $this->mink = new Mink(); $this->mink->registerSession('default', $session); $this->mink->setDefaultSessionName('default');