diff --git a/core/lib/Drupal/Core/Form/DialogFormTrait.php b/core/lib/Drupal/Core/Form/DialogFormTrait.php
new file mode 100644
index 0000000000..145a34ecfa
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/DialogFormTrait.php
@@ -0,0 +1,205 @@
+<?php
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Ajax\RedirectCommand;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseDialogCommand;
+use Drupal\Core\Url;
+
+/**
+ * Provides utilities for forms that want to be rendered in dialogs.
+ */
+trait DialogFormTrait {
+
+  /**
+   * Adds dialog support to a form.
+   *
+   * @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.
+   */
+  protected function buildFormDialog(array &$form, FormStateInterface $form_state, $create_cancel = FALSE) {
+    if (!$this->isDialog()) {
+      return;
+    }
+
+    $ajax_callback_added = FALSE;
+
+    if (!empty($form['actions']['submit'])) {
+      $form['actions']['submit']['#ajax'] = [
+        'callback' => '::submitFormDialog',
+        'event' => 'click',
+      ];
+      $ajax_callback_added = TRUE;
+    }
+
+    if ($create_cancel) {
+      $form['actions']['cancel'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Cancel'),
+        '#weight' => 100,
+      ];
+    }
+    if (!empty($form['actions']['cancel'])) {
+      // Replace 'Cancel' link button with a close dialog button.
+      $form['actions']['cancel'] = [
+        '#submit' => ['::noSubmit'],
+        '#limit_validation_errors' => [],
+        '#ajax' => [
+          'callback' => '::closeDialog',
+          'event' => 'click',
+        ],
+      ] + $form['actions']['cancel'];
+      $ajax_callback_added = TRUE;
+    }
+
+    if ($ajax_callback_added) {
+      $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
+      $form['#attributes']['id'] = 'dialog-form';
+    }
+  }
+
+  /**
+   * Empty submit #ajax submit callback.
+   *
+   * This allows modal dialog to using ::submitCallback to validate and submit
+   * the form via one ajax required.
+   *
+   * @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.
+   */
+  public function noSubmit(array &$form, FormStateInterface $form_state) {
+  }
+
+
+
+  /**
+   * Determines if the current request is for an AJAX dialog.
+   *
+   * @return bool
+   *   TRUE is the current request if for an AJAX dialog.
+   */
+  protected function isDialog() {
+    return in_array($this->getRequestWrapperFormat(), [
+      'drupal_ajax',
+      'drupal_dialog',
+      'drupal_modal',
+      'drupal_dialog.off_canvas',
+    ]);
+  }
+
+  /**
+   * 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('#dialog-form', $form);
+    }
+    else {
+      if ($redirect_url = $this->getRedirectUrl()) {
+        $command = new RedirectCommand($redirect_url->setAbsolute()->toString());
+      }
+      else {
+        return $this->closeDialog($form, $form_state);
+      }
+    }
+    return $response->addCommand($command);
+  }
+
+  /**
+   * Close 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 bool|\Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response that display validation error messages.
+   */
+  public function closeDialog(array &$form, FormStateInterface $form_state) {
+    return (new AjaxResponse())->addCommand(new CloseDialogCommand($this->getDialogSelector()));
+  }
+
+  /**
+   * @return mixed
+   */
+  protected function getRequestWrapperFormat() {
+    $wrapper_format = $this->getRequest()
+      ->get(MainContentViewSubscriber::WRAPPER_FORMAT);
+    return $wrapper_format;
+  }
+
+  /**
+   * Gets the form's redirect URL.
+   *
+   * Isolate a form's redirect URL/destination so that it can be used by
+   * ::submitFormDialog or ::submitForm.
+   *
+   * @return \Drupal\Core\Url|null
+   *   The redirect URL or NULL if dialog should just be closed.
+   */
+  protected function getRedirectUrl() {
+    return $this->getDestinationUrl();
+  }
+
+  /**
+   * Gets the URL from the destination service.
+   *
+   * @return \Drupal\Core\Url|null
+   *   The destination URL or NULL no destination available.
+   */
+  protected function getDestinationUrl() {
+    if ($destination = $this->getRedirectDestinationPath()) {
+      return Url::fromUserInput('/' . $destination);
+    }
+  }
+
+  /**
+   * Gets the redirect destination path if specified in request.
+   *
+   * \Drupal\Core\Routing\RedirectDestination::get() cannot be used directly
+   * because it will use <current> if 'destination' is not in the query string.
+   *
+   * @return string|null
+   *   The redirect path or NULL if it is not specified.
+   */
+  protected function getRedirectDestinationPath() {
+    if ($this->requestStack->getCurrentRequest()->get('destination')) {
+      return $this->getRedirectDestination()->get();
+    }
+  }
+
+  /**
+   * @return string
+   */
+  protected function getDialogSelector() {
+    if ($this->getRequestWrapperFormat() === 'drupal_dialog.off_canvas') {
+      return '#drupal-off-canvas';
+    }
+    else {
+      return '#drupal-modal';
+    }
+  }
+
+}
diff --git a/core/modules/outside_in/outside_in.libraries.yml b/core/modules/outside_in/outside_in.libraries.yml
index 5c388b82f1..574d7ed656 100644
--- a/core/modules/outside_in/outside_in.libraries.yml
+++ b/core/modules/outside_in/outside_in.libraries.yml
@@ -20,6 +20,8 @@ drupal.outside_in:
     - core/drupal
     - core/jquery.once
     - core/drupal.ajax
+    - core/drupalSettings
+    - core/jquery.form
 drupal.off_canvas:
   version: VERSION
   js:
diff --git a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php
index f97f79ae05..58d287fed8 100644
--- a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php
+++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php
@@ -5,6 +5,7 @@
 use Drupal\block\BlockForm;
 use Drupal\block\BlockInterface;
 use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Form\DialogFormTrait;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginWithFormsInterface;
 
@@ -16,6 +17,8 @@
  */
 class BlockEntityOffCanvasForm extends BlockForm {
 
+  use DialogFormTrait;
+
   /**
    * Provides a title callback to get the block's admin label.
    *
@@ -109,4 +112,24 @@ 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);
+    $this->buildFormDialog($form, $form_state);
+    return $form;
+  }
+
 }
diff --git a/core/modules/outside_in/tests/modules/off_canvas_test/off_canvas_test.routing.yml b/core/modules/outside_in/tests/modules/off_canvas_test/off_canvas_test.routing.yml
index 4ae56c4ee8..f0b3a43653 100644
--- a/core/modules/outside_in/tests/modules/off_canvas_test/off_canvas_test.routing.yml
+++ b/core/modules/outside_in/tests/modules/off_canvas_test/off_canvas_test.routing.yml
@@ -27,3 +27,10 @@ off_canvas_test.dialog_links:
     _controller: '\Drupal\off_canvas_test\Controller\TestController::otherDialogLinks'
   requirements:
     _access: 'TRUE'
+
+off_canvas_test.form:
+  path: '/off-canvas-form'
+  defaults:
+    _form: '\Drupal\off_canvas_test\Form\TestForm'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/outside_in/tests/modules/off_canvas_test/src/Controller/TestController.php b/core/modules/outside_in/tests/modules/off_canvas_test/src/Controller/TestController.php
index 6164b06fc2..1208edf2a0 100644
--- a/core/modules/outside_in/tests/modules/off_canvas_test/src/Controller/TestController.php
+++ b/core/modules/outside_in/tests/modules/off_canvas_test/src/Controller/TestController.php
@@ -92,6 +92,42 @@ public function linksDisplay() {
           ],
         ],
       ],
+      'off_canvas_form' => [
+        '#title' => 'Show form!',
+        '#type' => 'link',
+        '#url' => Url::fromRoute(
+          'off_canvas_test.form',
+          [],
+          ['query' => ['destination' => 'off-canvas-test-links']]
+        ),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'dialog',
+          'data-dialog-renderer' => 'off_canvas',
+        ],
+        '#attached' => [
+          'library' => [
+            'outside_in/drupal.outside_in',
+          ],
+        ],
+      ],
+      'off_canvas_form_no_dest' => [
+        '#title' => 'Show form: no destination!',
+        '#type' => 'link',
+        '#url' => Url::fromRoute(
+          'off_canvas_test.form'
+        ),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'dialog',
+          'data-dialog-renderer' => 'off_canvas',
+        ],
+        '#attached' => [
+          'library' => [
+            'outside_in/drupal.outside_in',
+          ],
+        ],
+      ],
     ];
   }
 
diff --git a/core/modules/outside_in/tests/modules/off_canvas_test/src/Form/TestForm.php b/core/modules/outside_in/tests/modules/off_canvas_test/src/Form/TestForm.php
new file mode 100644
index 0000000000..64b14e8e6c
--- /dev/null
+++ b/core/modules/outside_in/tests/modules/off_canvas_test/src/Form/TestForm.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\off_canvas_test\Form;
+
+use Drupal\Core\Form\DialogFormTrait;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Just a test form.
+ */
+class TestForm extends FormBase {
+
+  use DialogFormTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return "off_canvas_test_form";
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['force_error'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Force error?'),
+    ];
+    $form['actions'] = [
+      '#type' => 'actions',
+      'submit' => [
+        '#type' => 'submit',
+        '#value' => $this->t('Submit'),
+      ],
+      'cancel' => [
+        '#type' => 'submit',
+        '#value' => $this->t('Cancel'),
+      ],
+    ];
+    $this->buildFormDialog($form, $form_state);
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
+    if ($form_state->getValue('force_error')) {
+      $form_state->setErrorByName('force_error', 'Validation error');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    drupal_set_message('submitted');
+  }
+
+}
diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php
index 2f64d9fe2c..bc39787b09 100644
--- a/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php
@@ -120,4 +120,57 @@ protected function getTestThemes() {
     return array_merge(parent::getTestThemes(), ['seven']);
   }
 
+  /**
+   * Tests form errors in the Off-Canvas dialog.
+   */
+  public function testFormErrors() {
+    $web_assert = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    // Submit form with no error and sending a destination.
+    $this->drupalGet('/off-canvas-test-links');
+    $page->clickLink('Show form!');
+    $this->waitForOffCanvasToOpen();
+    $page->pressButton('Submit');
+    $web_assert->assertWaitOnAjaxRequest();
+    // Make sure the changes are present.
+    $this->assertNotEmpty($web_assert->waitForElement('css', 'div.messages.messages--status:contains(submitted)'));
+    $web_assert->elementNotContains('css', 'body', 'Validation error');
+
+    // Submit form with an error and sending a destination.
+    $this->drupalGet('/off-canvas-test-links');
+    $page->clickLink('Show form!');
+    $this->waitForOffCanvasToOpen();
+    $page->checkField('Force error?');
+    $page->pressButton('Submit');
+    $web_assert->assertWaitOnAjaxRequest();
+    $web_assert->elementNotContains('css', 'body', 'submitted');
+    $web_assert->elementContains('css', '#drupal-off-canvas', 'Validation error');
+
+    // Submit form with no error and NOT sending a destination.
+    $this->drupalGet('/off-canvas-test-links');
+    $page->clickLink('Show form: no destination!');
+    $this->waitForOffCanvasToOpen();
+    $page->pressButton('Submit');
+    $web_assert->assertWaitOnAjaxRequest();
+    // Make sure the changes are present.
+    $this->assertEmpty($web_assert->waitForElement('css', 'div.messages.messages--status:contains(submitted)'));
+    $web_assert->elementNotContains('css', 'body', 'Validation error');
+    // If no validation error and no destination provided page will not be
+    // redirected but the dialog should be closed.
+    $this->waitForNoElement('css', '#drupal-off-canvas');
+
+    $this->drupalGet('/off-canvas-test-links');
+
+    // Submit form with error and NOT sending a destination.
+    $this->drupalGet('/off-canvas-test-links');
+    $page->clickLink('Show form: no destination!');
+    $this->waitForOffCanvasToOpen();
+    $page->checkField('Force error?');
+    $page->pressButton('Submit');
+    $web_assert->assertWaitOnAjaxRequest();
+    $web_assert->elementNotContains('css', 'body', 'submitted');
+    $web_assert->elementContains('css', '#drupal-off-canvas', 'Validation error');
+  }
+
 }
diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php
index ba865c7a6f..79bad45d07 100644
--- a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php
@@ -85,7 +85,7 @@ public function testBlocks($block_plugin, $new_page_text, $element_selector, $la
           $this->waitForNoElement("#toolbar-administration a.is-active");
         }
         $page->find('css', $toolbar_item)->click();
-        $web_assert->waitForElementVisible('css', "{$toolbar_item}.is-active");
+        $this->assertElementVisibleAfterWait('css', "{$toolbar_item}.is-active");
       }
       $this->enableEditMode();
       if (isset($toolbar_item)) {
@@ -114,10 +114,8 @@ 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);
       }
 
       $this->openBlockForm($block_selector);
@@ -131,7 +129,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 +152,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 +160,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',
@@ -227,8 +225,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->waitForToolbarToLoad();
     $this->click($block_selector);
     $this->waitForOffCanvasToOpen();
@@ -276,7 +286,7 @@ public function testQuickEditLinks() {
         $this->drupalGet('node/' . $node->id());
         // Waiting for Toolbar module.
         // @todo Remove the hack after https://www.drupal.org/node/2542050.
-        $web_assert->waitForElementVisible('css', '.toolbar-fixed');
+        $this->assertElementVisibleAfterWait('css', '.toolbar-fixed');
         // Waiting for Toolbar animation.
         $web_assert->assertWaitOnAjaxRequest();
         // The 2nd page load we should already be in edit mode.
@@ -285,7 +295,7 @@ public function testQuickEditLinks() {
         }
         // In Edit mode clicking field should open QuickEdit toolbar.
         $page->find('css', $body_selector)->click();
-        $web_assert->waitForElementVisible('css', $quick_edit_selector);
+        $this->assertElementVisibleAfterWait('css', $quick_edit_selector);
 
         $this->disableEditMode();
         // Exiting Edit mode should close QuickEdit toolbar.
@@ -296,7 +306,7 @@ public function testQuickEditLinks() {
         $this->enableEditMode();
         $this->openBlockForm($block_selector);
         $page->find('css', $body_selector)->click();
-        $web_assert->waitForElementVisible('css', $quick_edit_selector);
+        $this->assertElementVisibleAfterWait('css', $quick_edit_selector);
         // Off-canvas dialog should be closed when opening QuickEdit toolbar.
         $this->waitForOffCanvasToClose();
 
@@ -310,7 +320,7 @@ public function testQuickEditLinks() {
       $this->disableEditMode();
       // Open QuickEdit toolbar before going into Edit mode.
       $this->clickContextualLink($node_selector, "Quick edit");
-      $web_assert->waitForElementVisible('css', $quick_edit_selector);
+      $this->assertElementVisibleAfterWait('css', $quick_edit_selector);
       // Open off-canvas and enter Edit mode via contextual link.
       $this->clickContextualLink($block_selector, "Quick edit");
       $this->waitForOffCanvasToOpen();
@@ -319,7 +329,7 @@ public function testQuickEditLinks() {
       // Open QuickEdit toolbar via contextual link while in Edit mode.
       $this->clickContextualLink($node_selector, "Quick edit", FALSE);
       $this->waitForOffCanvasToClose();
-      $web_assert->waitForElementVisible('css', $quick_edit_selector);
+      $this->assertElementVisibleAfterWait('css', $quick_edit_selector);
       $this->disableEditMode();
     }
   }
diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php
index 2dcc40fb44..129aaec7ec 100644
--- a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php
@@ -42,7 +42,7 @@ public function enableTheme($theme) {
   protected function waitForOffCanvasToOpen() {
     $web_assert = $this->assertSession();
     $web_assert->assertWaitOnAjaxRequest();
-    $web_assert->waitForElementVisible('css', '#drupal-off-canvas');
+    $this->assertElementVisibleAfterWait('css', '#drupal-off-canvas');
   }
 
   /**
@@ -125,7 +125,7 @@ protected function waitForToolbarToLoad() {
     $web_assert = $this->assertSession();
     // Waiting for Toolbar module.
     // @todo Remove the hack after https://www.drupal.org/node/2542050.
-    $web_assert->waitForElementVisible('css', '.toolbar-fixed');
+    $this->assertElementVisibleAfterWait('css', '.toolbar-fixed');
     // Waiting for Toolbar animation.
     $web_assert->assertWaitOnAjaxRequest();
   }
@@ -140,4 +140,19 @@ protected function getTestThemes() {
     return ['bartik', 'stark', 'classy', 'stable'];
   }
 
+  /**
+   * Asserts the specified selector is visible after a wait.
+   *
+   * @param string $selector
+   *   The selector engine name. See ElementInterface::findAll() for the
+   *   supported selectors.
+   * @param string|array $locator
+   *   The selector locator.
+   * @param int $timeout
+   *   (Optional) Timeout in milliseconds, defaults to 10000.
+   */
+  protected function assertElementVisibleAfterWait($selector, $locator, $timeout = 10000) {
+    $this->assertNotEmpty($this->assertSession()->waitForElementVisible($selector, $locator, $timeout));
+  }
+
 }
