diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js index 497393f..6a20cd2 100644 --- a/core/modules/ckeditor/js/ckeditor.js +++ b/core/modules/ckeditor/js/ckeditor.js @@ -273,6 +273,20 @@ } }); + // Redirect on hash change when the original hash has an associated CKEditor. + function redirectTextareaFragmentToCKEditorInstance() { + var hash = location.hash.substr(1); + var element = document.getElementById(hash); + if (element) { + var editor = CKEDITOR.dom.element.get(element).getEditor(); + if (editor) { + var id = editor.container.getAttribute('id'); + location.hash = '#' + id; + } + } + } + $(window).on('hashchange', redirectTextareaFragmentToCKEditorInstance); + // Set the CKEditor cache-busting string to the same value as Drupal. CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp; diff --git a/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php b/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php new file mode 100644 index 0000000..3c529c7 --- /dev/null +++ b/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php @@ -0,0 +1,109 @@ + 'filtered_html', + 'name' => 'Filtered HTML', + 'weight' => 0, + ]); + $filtered_html_format->save(); + + Editor::create([ + 'format' => 'filtered_html', + 'editor' => 'ckeditor', + ])->save(); + + // Create a node type for testing. + NodeType::create(['type' => 'page', 'name' => 'page'])->save(); + + $field_storage = FieldStorageConfig::loadByName('node', 'body'); + + // Create a body field instance for the 'page' node type. + FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'page', + 'label' => 'Body', + 'settings' => ['display_summary' => TRUE], + 'required' => TRUE, + ])->save(); + + // Assign widget settings for the 'default' form mode. + EntityFormDisplay::create([ + 'targetEntityType' => 'node', + 'bundle' => 'page', + 'mode' => 'default', + 'status' => TRUE, + ])->setComponent('body', ['type' => 'text_textarea_with_summary']) + ->save(); + + $this->account = $this->drupalCreateUser([ + 'administer nodes', + 'create page content', + 'use text format filtered_html', + ]); + $this->drupalLogin($this->account); + } + + /** + * Tests if the fragment link to a textarea works with CKEditor enabled. + */ + public function testFragmentLink() { + $session = $this->getSession(); + $web_assert = $this->assertSession(); + $ckeditor_id = '#cke_edit-body-0-value'; + + $this->drupalGet('node/add/page'); + + $session->getPage(); + + // Add a bottom margin to the title field to be sure the body field is not + // visible. PhantomJS runs with a resolution of 1024x768px. + $session->executeScript("document.getElementById('edit-title-0-value').style.marginBottom = '800px';"); + + // Check that the CKEditor-enabled body field is currently not visible in + // the viewport. + $web_assert->assertNotVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor-enabled body field is visible.'); + + // Trigger a hash change with as target the hidden textarea. + $session->executeScript("location.hash = '#edit-body-0-value';"); + + // Check that the CKEditor-enabled body field is visible in the viewport. + $web_assert->assertVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor-enabled body field is not visible.'); + } + +} diff --git a/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditorTest.php b/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditorTest.php new file mode 100644 index 0000000..4e83090 --- /dev/null +++ b/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditorTest.php @@ -0,0 +1,127 @@ + 'filtered_html', + 'name' => 'Filtered HTML', + 'weight' => 0, + ]); + $filtered_html_format->save(); + + Editor::create([ + 'format' => 'filtered_html', + 'editor' => 'ckeditor', + ])->save(); + + // Create a node type for testing. + NodeType::create(['type' => 'page', 'name' => 'page'])->save(); + + $field_storage = FieldStorageConfig::loadByName('node', 'body'); + + // Create a body field instance for the 'page' node type. + FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'page', + 'label' => 'Body', + 'settings' => ['display_summary' => TRUE], + 'required' => TRUE, + ])->save(); + + // Assign widget settings for the 'default' form mode. + EntityFormDisplay::create([ + 'targetEntityType' => 'node', + 'bundle' => 'page', + 'mode' => 'default', + 'status' => TRUE, + ])->setComponent('body', ['type' => 'text_textarea_with_summary']) + ->save(); + + $account = $this->drupalCreateUser([ + 'administer nodes', + 'create page content', + 'use text format filtered_html', + ]); + $this->drupalLogin($account); + } + + /** + * Tests if the fragment link to a textarea works with CKEditor enabled. + */ + public function testFragmentLink() { + $session = $this->getSession(); + + $this->drupalGet('node/add/page'); + + $page = $this->getSession()->getPage(); + + // Only enter a title in the node add form and leave the body field empty. + $edit = ['edit-title-0-value' => 'Test inline form error with CKEditor']; + + $this->submitForm($edit, 'Save and publish'); + + // Add a bottom margin to the title field to be sure the body field is not + // visible. PhantomJS runs with a resolution of 1024x768px. + $session->executeScript("document.getElementById('edit-title-0-value').style.marginBottom = '800px';"); + + // Javascript to test if top of the CKEditor-enabled body field is visible + // in the viewport. The NodeElement::isVisible() method doesn't take the + // viewport into account. + $visibility_check_javascript = <<= 0 && + r.top < (w.innerHeight || e.clientHeight) + ); + }()); +JS; + + // Check that the CKEditor-enabled body field is currently not visible in + // the viewport. + $this->assertFalse($session->evaluateScript($visibility_check_javascript), 'CKEditor-enabled body field is not visible.'); + + // Check if we can find the error fragment link within the errors summary + // message. + $errors_link = $page->find('css', '.messages--error a[href=\#edit-body-0-value]'); + $this->assertTrue($errors_link->isVisible(), 'Error fragment link is visible.'); + + $errors_link->click(); + + // Check that the CKEditor-enabled body field is visible in the viewport. + $this->assertTrue($session->evaluateScript($visibility_check_javascript), 'CKEditor-enabled body field is visible.'); + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php index 8d3ff47..26e4278 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php @@ -2,6 +2,10 @@ namespace Drupal\FunctionalJavascriptTests; +use Behat\Mink\Element\NodeElement; +use Behat\Mink\Exception\ElementHtmlException; +use Behat\Mink\Exception\ElementNotFoundException; +use Behat\Mink\Exception\UnsupportedDriverActionException; use Drupal\Tests\WebAssert; /** @@ -28,4 +32,199 @@ public function assertWaitOnAjaxRequest($timeout = 10000, $message = 'Unable to } } + /** + * Test that a node, or it's specific corner, is visible in the viewport. + * + * Note: Always set the viewport size. This can be done with a PhantomJS + * startup parameter or in your test with \Behat\Mink\Session->resizeWindow(). + * Drupal CI Javascript tests by default use a viewport of 1024x768px. + * + * @param string $selector_type + * The element selector type (CSS, XPath). + * @param string|array $selector + * The element selector. Note: the first found element is used. + * @param bool|string $corner + * (Optional) The corner to test: + * topLeft, topRight, bottomRight, bottomLeft. + * Or FALSE to check the complete element (default). + * @param string $message + * (optional) A message for the exception. + * + * @throws \Behat\Mink\Exception\ElementHtmlException + * When the element doesn't exist. + * @throws \Behat\Mink\Exception\ElementNotFoundException + * When the element is not visible in the viewport. + */ + public function assertVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is not visible in the viewport.') { + $node = $this->session->getPage()->find($selector_type, $selector); + if ($node === NULL) { + if (is_array($selector)) { + $selector = implode(' ', $selector); + } + throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector); + } + + // Check if the node is visible on the page, which is a prerequisite of + // being visible in the viewport. + if (!$node->isVisible()) { + throw new ElementHtmlException($message, $this->session->getDriver(), $node); + } + + $result = $this->checkNodeVisibilityInViewport($node, $corner); + + if (!$result) { + throw new ElementHtmlException($message, $this->session->getDriver(), $node); + } + } + + /** + * Test that a node, or its specific corner, is not visible in the viewport. + * + * Note: the node should exist in the page, otherwise this assertion fails. + * + * @param string $selector_type + * The element selector type (CSS, XPath). + * @param string|array $selector + * The element selector. Note: the first found element is used. + * @param bool|string $corner + * (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft. + * Or FALSE to check the complete element (default). + * @param string $message + * (optional) A message for the exception. + * + * @throws \Behat\Mink\Exception\ElementHtmlException + * When the element doesn't exist. + * @throws \Behat\Mink\Exception\ElementNotFoundException + * When the element is not visible in the viewport. + * + * @see \Drupal\FunctionalJavascriptTests\JSWebAssert::assertVisibleInViewport() + */ + public function assertNotVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is visible in the viewport.') { + $node = $this->session->getPage()->find($selector_type, $selector); + if ($node === NULL) { + if (is_array($selector)) { + $selector = implode(' ', $selector); + } + throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector); + } + + $result = $this->checkNodeVisibilityInViewport($node, $corner); + + if ($result) { + throw new ElementHtmlException($message, $this->session->getDriver(), $node); + } + } + + /** + * Check the visibility of a node, or it's specific corner. + * + * @param \Behat\Mink\Element\NodeElement $node + * A valid node. + * @param bool|string $corner + * (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft. + * Or FALSE to check the complete element (default). + * + * @return bool + * Returns TRUE if the node is visible in the viewport, FALSE otherwise. + * + * @throws \Behat\Mink\Exception\UnsupportedDriverActionException + * When an invalid corner specification is given. + */ + private function checkNodeVisibilityInViewport(NodeElement $node, $corner = FALSE) { + $xpath = $node->getXpath(); + + // Build the Javascript to test if the complete element or a specific corner + // is in the viewport. + switch ($corner) { + case 'topLeft': + $test_javascript_function = <<= 0 && + r.top <= ly && + r.left >= 0 && + r.left <= lx + ) + } +JS; + break; + + case 'topRight': + $test_javascript_function = <<= 0 && + r.top <= ly && + r.right >= 0 && + r.right <= lx + ); + } +JS; + break; + + case 'bottomRight': + $test_javascript_function = <<= 0 && + r.bottom <= ly && + r.right >= 0 && + r.right <= lx + ); + } +JS; + break; + + case 'bottomLeft': + $test_javascript_function = <<= 0 && + r.bottom <= ly && + r.left >= 0 && + r.left <= lx + ); + } +JS; + break; + + case FALSE: + $test_javascript_function = <<= 0 && + r.left >= 0 && + r.bottom <= ly && + r.right <= lx + ); + } +JS; + break; + + // Throw an exception if an invalid corner parameter is given. + default: + throw new UnsupportedDriverActionException($corner, $this->session->getDriver()); + } + + // Build the full Javascript test. The shared logic gets the corner + // specific test logic injected. + $full_javascript_visibility_test = <<session->evaluateScript($full_javascript_visibility_test); + } + }