diff --git a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php index 19a701638d..0b813eaca1 100644 --- a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php +++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php @@ -4,9 +4,15 @@ use Drupal\block\BlockForm; use Drupal\block\BlockInterface; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\RedirectCommand; +use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Block\BlockPluginInterface; +use Drupal\Core\Form\DialogFormTrait; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginWithFormsInterface; +use Drupal\Core\Url; +use Drupal\outside_in\OffCanvasFormDialogTrait; /** * Provides form for block instance forms when used in the off-canvas dialog. @@ -111,4 +117,80 @@ protected function getPluginForm(BlockPluginInterface $block) { return $block; } + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + // \Drupal\block\BlockForm::submitForm() always redirects to block listing + // via \Drupal\Core\Form\FormStateInterface::setRedirect(). This method + // does not work with Ajax submit. + $form_state->disableRedirect(); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = parent::buildForm($form, $form_state); + $form['actions']['submit']['#ajax'] = [ + 'callback' => '::submitFormDialog', + 'event' => 'click', + ]; + + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $form['#attributes']['id'] = 'off-canvas-form'; + return $form; + } + + /** + * Submit form dialog #ajax callback. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * An AJAX response that display validation error messages or redirects + * to a URL + */ + public function submitFormDialog(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + if ($form_state->hasAnyErrors()) { + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -1000, + ]; + $command = new ReplaceCommand('#off-canvas-form', $form); + } + else { + if ($redirect_url = $this->getRedirectUrl()) { + $command = new RedirectCommand($redirect_url->setAbsolute()->toString()); + } + else { + // Settings Tray always provides a destination. + throw new \Exception("No destination provide for Settings Tray form"); + } + } + return $response->addCommand($command); + } + + /** + * Gets the form's redirect URL from 'destination' provide in the request. + * + * @return \Drupal\Core\Url|null + * The redirect URL or NULL if dialog should just be closed. + */ + protected function getRedirectUrl() { + // \Drupal\Core\Routing\RedirectDestination::get() cannot be used directly + // because it will use if 'destination' is not in the query + // string. + if ($this->getRequest()->get('destination')) { + if ($destination = $this->getRedirectDestination()->get()) { + return Url::fromUserInput('/' . $destination); + } + } + } + } diff --git a/core/modules/outside_in/tests/modules/outside_in_test/outside_in_test.info.yml b/core/modules/outside_in/tests/modules/outside_in_test/outside_in_test.info.yml new file mode 100644 index 0000000000..590f559329 --- /dev/null +++ b/core/modules/outside_in/tests/modules/outside_in_test/outside_in_test.info.yml @@ -0,0 +1,9 @@ +name: 'Settings Tray Test' +type: module +description: 'Provides Settings Tray test functionality.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - block + - outside_in diff --git a/core/modules/outside_in/tests/modules/outside_in_test/outside_in_test.module b/core/modules/outside_in/tests/modules/outside_in_test/outside_in_test.module new file mode 100644 index 0000000000..5410cfb535 --- /dev/null +++ b/core/modules/outside_in/tests/modules/outside_in_test/outside_in_test.module @@ -0,0 +1,30 @@ +getValue(['settings', 'label']) == 'Block label') { + $form_state->setError($element, 'Meta label error.'); + } +} diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php index 04fc230f5e..131e27b937 100644 --- a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php @@ -32,6 +32,7 @@ class OutsideInBlockFormTest extends OutsideInJavascriptTestBase { 'quickedit', 'search', 'block_content', + 'outside_in_test', // Add test module to override CSS pointer-events properties because they // cause test failures. 'outside_in_test_css', @@ -114,10 +115,9 @@ public function testBlocks($block_plugin, $new_page_text, $element_selector, $la if (isset($new_page_text)) { $page->pressButton($button_text); // Make sure the changes are present. - // @todo Use a wait method that will take into account the form submitting - // and all JavaScript activity. https://www.drupal.org/node/2837676 - // The use \Behat\Mink\WebAssert::pageTextContains to check text. - $this->assertJsCondition('jQuery("' . $block_selector . ' ' . $label_selector . '").html() == "' . $new_page_text . '"'); + $new_page_text_locator = "$block_selector $label_selector:contains($new_page_text)"; + $this->assertElementVisibleAfterWait('css', $new_page_text_locator); + $web_assert->assertWaitOnAjaxRequest(); } $this->openBlockForm($block_selector); @@ -131,7 +131,7 @@ public function testBlocks($block_plugin, $new_page_text, $element_selector, $la // Open block form by clicking a element inside the block. // This confirms that default action for links and form elements is // suppressed. - $this->openBlockForm("$block_selector {$element_selector}"); + $this->openBlockForm("$block_selector {$element_selector}", $block_selector); $web_assert->elementTextContains('css', '.contextual-toolbar-tab button', 'Editing'); $web_assert->elementAttributeContains('css', '.dialog-off-canvas__main-canvas', 'class', 'js-outside-in-edit-mode'); // Simulate press the Escape key. @@ -154,7 +154,7 @@ public function providerTestBlocks() { $blocks = [ 'block-powered' => [ 'block_plugin' => 'system_powered_by_block', - 'new_page_text' => 'Can you imagine anyone showing the label on this block?', + 'new_page_text' => 'Can you imagine anyone showing the label on this block', 'element_selector' => 'span a', 'label_selector' => 'h2', 'button_text' => 'Save Powered by Drupal', @@ -162,7 +162,7 @@ public function providerTestBlocks() { ], 'block-branding' => [ 'block_plugin' => 'system_branding_block', - 'new_page_text' => 'The site that will live a very short life.', + 'new_page_text' => 'The site that will live a very short life', 'element_selector' => "a[rel='home']:last-child", 'label_selector' => "a[rel='home']:last-child", 'button_text' => 'Save Site branding', @@ -192,6 +192,7 @@ protected function enableEditMode() { * Disables edit mode by pressing edit button in the toolbar. */ protected function disableEditMode() { + $this->assertSession()->assertWaitOnAjaxRequest(); $this->pressToolbarEditButton(); $this->assertEditModeDisabled(); } @@ -225,8 +226,20 @@ protected function assertOffCanvasBlockFormIsValid() { * * @param string $block_selector * A css selector selects the block or an element within it. + * @param string $contextual_link_container + * The element that contains the contextual links. If none provide the + * $block_selector will be used. */ - protected function openBlockForm($block_selector) { + protected function openBlockForm($block_selector, $contextual_link_container = '') { + if (!$contextual_link_container) { + $contextual_link_container = $block_selector; + } + // Ensure that contextual link element is present because this is required + // to open the off-canvas dialog in edit mode. + $contextual_link = $this->assertSession()->waitForElement('css', "$contextual_link_container .contextual-links a"); + $this->assertNotEmpty($contextual_link); + // Ensure that all other Ajax activity is completed. + $this->assertSession()->assertWaitOnAjaxRequest(); $this->click($block_selector); $this->waitForOffCanvasToOpen(); $this->assertOffCanvasBlockFormIsValid(); @@ -490,4 +503,34 @@ protected function isLabelInputVisible() { return $this->getSession()->getPage()->find('css', static::LABEL_INPUT_SELECTOR)->isVisible(); } + /** + * Test that validation errors appear in the off-canvas dialog. + */ + public function testValidationMessages() { + $page = $this->getSession()->getPage(); + $web_assert = $this->assertSession(); + foreach ($this->getTestThemes() as $theme) { + $this->enableTheme($theme); + $block = $this->placeBlock('system_powered_by_block'); + $this->drupalGet('user'); + $this->enableEditMode(); + $this->openBlockForm($this->getBlockSelector($block)); + + // Confirm "Display Title" is not checked. + $web_assert->checkboxNotChecked('settings[label_display]'); + // Confirm Title is not visible. + $this->assertEquals($this->isLabelInputVisible(), FALSE, 'Label is not visible'); + $page->checkField('settings[label_display]'); + $this->assertEquals($this->isLabelInputVisible(), TRUE, 'Label is visible'); + // Use label that will trigger validation error. + // @see _outside_in_test_validate_title + $page->fillField('settings[label]', 'Block label'); + $page->pressButton('Save Powered by Drupal'); + $web_assert->assertWaitOnAjaxRequest(); + $web_assert->elementContains('css', '#drupal-off-canvas', 'Meta label error'); + $this->disableEditMode(); + $block->delete(); + } + } + }