diff --git a/core/modules/contextual/tests/modules/contextual_test/contextual_test.info.yml b/core/modules/contextual/tests/modules/contextual_test/contextual_test.info.yml
new file mode 100644
index 0000000000..1d8d4d5549
--- /dev/null
+++ b/core/modules/contextual/tests/modules/contextual_test/contextual_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Contextual Test'
+type: module
+description: 'Provides test contextual links.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - contextual
diff --git a/core/modules/contextual/tests/modules/contextual_test/contextual_test.links.contextual.yml b/core/modules/contextual/tests/modules/contextual_test/contextual_test.links.contextual.yml
new file mode 100644
index 0000000000..35e03b7333
--- /dev/null
+++ b/core/modules/contextual/tests/modules/contextual_test/contextual_test.links.contextual.yml
@@ -0,0 +1,4 @@
+contextual_test:
+  title: 'Test Link'
+  route_name: 'contextual_test'
+  group: 'contextual_test'
diff --git a/core/modules/contextual/tests/modules/contextual_test/contextual_test.module b/core/modules/contextual/tests/modules/contextual_test/contextual_test.module
new file mode 100644
index 0000000000..b8f2e60f51
--- /dev/null
+++ b/core/modules/contextual/tests/modules/contextual_test/contextual_test.module
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Provides test contextual link on blocks.
+ */
+
+use Drupal\Core\Block\BlockPluginInterface;
+
+/**
+ * Implements hook_block_view_alter().
+ */
+function contextual_test_block_view_alter(array &$build, BlockPluginInterface $block) {
+  $build['#contextual_links']['contextual_test'] = [
+    'route_parameters' => [],
+  ];
+}
diff --git a/core/modules/contextual/tests/modules/contextual_test/contextual_test.routing.yml b/core/modules/contextual/tests/modules/contextual_test/contextual_test.routing.yml
new file mode 100644
index 0000000000..78ca61da6e
--- /dev/null
+++ b/core/modules/contextual/tests/modules/contextual_test/contextual_test.routing.yml
@@ -0,0 +1,6 @@
+contextual_test:
+  path: '/contextual-tests'
+  defaults:
+    _controller: '\Drupal\contextual_test\Controller\TestController::render'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/contextual/tests/modules/contextual_test/src/Controller/TestController.php b/core/modules/contextual/tests/modules/contextual_test/src/Controller/TestController.php
new file mode 100644
index 0000000000..099f00c51a
--- /dev/null
+++ b/core/modules/contextual/tests/modules/contextual_test/src/Controller/TestController.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\contextual_test\Controller;
+
+/**
+ * Test controller to provide a callback for the contextual link.
+ */
+class TestController {
+
+  /**
+   * Callback for the contextual link.
+   *
+   * @return array
+   *   Render array.
+   */
+  public function render() {
+    return [
+      '#type' => 'markup',
+      '#markup' => 'Everything is contextual!',
+    ];
+  }
+
+}
diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinkClickTrait.php b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinkClickTrait.php
new file mode 100644
index 0000000000..2866e91cbf
--- /dev/null
+++ b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinkClickTrait.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\Tests\contextual\FunctionalJavascript;
+
+/**
+ * Functions for testing contextual links.
+ */
+trait ContextualLinkClickTrait {
+
+  /**
+   * Clicks a contextual link.
+   *
+   * @param string $selector
+   *   The selector for the element that contains the contextual link.
+   * @param string $link_locator
+   *   The link id, title, or text.
+   * @param bool $force_visible
+   *   If true then the button will be forced to visible so it can be clicked.
+   */
+  protected function clickContextualLink($selector, $link_locator, $force_visible = TRUE) {
+    if ($force_visible) {
+      $this->toggleContextualTriggerVisibility($selector);
+    }
+
+    $element = $this->getSession()->getPage()->find('css', $selector);
+    $element->find('css', '.contextual button')->press();
+    $element->findLink($link_locator)->click();
+
+    if ($force_visible) {
+      $this->toggleContextualTriggerVisibility($selector);
+    }
+  }
+
+  /**
+   * Toggles the visibility of a contextual trigger.
+   *
+   * @param string $selector
+   *   The selector for the element that contains the contextual link.
+   */
+  protected function toggleContextualTriggerVisibility($selector) {
+    // Hovering over the element itself with should be enough, but does not
+    // work. Manually remove the visually-hidden class.
+    $this->getSession()->executeScript("jQuery('{$selector} .contextual .trigger').toggleClass('visually-hidden');");
+  }
+
+}
diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php
index 12ce10db8b..15edb62130 100644
--- a/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php
+++ b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php
@@ -12,6 +12,8 @@
  */
 class ContextualLinksTest extends JavascriptTestBase {
 
+  use ContextualLinkClickTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -23,6 +25,7 @@ class ContextualLinksTest extends JavascriptTestBase {
   protected function setUp() {
     parent::setUp();
 
+    $this->drupalLogin($this->createUser(['access contextual links']));
     $this->placeBlock('system_branding_block', ['id' => 'branding']);
   }
 
@@ -30,10 +33,6 @@ protected function setUp() {
    * Tests the visibility of contextual links.
    */
   public function testContextualLinksVisibility() {
-    $this->drupalLogin($this->drupalCreateUser([
-      'access contextual links'
-    ]));
-
     $this->drupalGet('user');
     $contextualLinks = $this->assertSession()->waitForElement('css', '.contextual button');
     $this->assertEmpty($contextualLinks);
@@ -59,4 +58,26 @@ public function testContextualLinksVisibility() {
     $this->assertNotEmpty($contextualLinks);
   }
 
+  /**
+   * Test clicking contextual links.
+   */
+  public function testContextualLinksClick() {
+    $this->container->get('module_installer')->install(['contextual_test']);
+    // Test clicking contextual link without toolbar.
+    $this->drupalGet('user');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->clickContextualLink('#block-branding', 'Test Link');
+    $this->assertSession()->pageTextContains('Everything is contextual!');
+
+    // Test clicking contextual link with toolbar.
+    $this->container->get('module_installer')->install(['toolbar']);
+    $this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), ['access toolbar']);
+    $this->drupalGet('user');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // Click "Edit" in toolbar to show contextual links.
+    $this->getSession()->getPage()->find('css', '.contextual-toolbar-tab button')->press();
+    $this->clickContextualLink('#block-branding', 'Test Link', FALSE);
+    $this->assertSession()->pageTextContains('Everything is contextual!');
+  }
 }
diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php
index d2881eb599..31b4d7ec77 100644
--- a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php
+++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php
@@ -7,6 +7,7 @@
 use Drupal\block_content\Entity\BlockContentType;
 use Drupal\settings_tray_test\Plugin\Block\SettingsTrayFormAnnotationIsClassBlock;
 use Drupal\settings_tray_test\Plugin\Block\SettingsTrayFormAnnotationNoneBlock;
+use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
 use Drupal\user\Entity\Role;
 
 /**
@@ -16,6 +17,8 @@
  */
 class SettingsTrayBlockFormTest extends SettingsTrayJavascriptTestBase {
 
+  use ContextualLinkClickTrait;
+
   const TOOLBAR_EDIT_LINK_SELECTOR = '#toolbar-bar div.contextual-toolbar-tab button';
 
   const LABEL_INPUT_SELECTOR = 'input[data-drupal-selector="edit-settings-label"]';
diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayJavascriptTestBase.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayJavascriptTestBase.php
index 7f7f3943af..0872a88602 100644
--- a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayJavascriptTestBase.php
+++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayJavascriptTestBase.php
@@ -76,48 +76,6 @@ protected function waitForNoElement($selector, $timeout = 10000) {
     $this->assertJsCondition($condition, $timeout);
   }
 
-  /**
-   * Clicks a contextual link.
-   *
-   * @todo Remove this function when related trait added in
-   *   https://www.drupal.org/node/2821724.
-   *
-   * @param string $selector
-   *   The selector for the element that contains the contextual link.
-   * @param string $link_locator
-   *   The link id, title, or text.
-   * @param bool $force_visible
-   *   If true then the button will be forced to visible so it can be clicked.
-   */
-  protected function clickContextualLink($selector, $link_locator, $force_visible = TRUE) {
-    if ($force_visible) {
-      $this->toggleContextualTriggerVisibility($selector);
-    }
-
-    $element = $this->getSession()->getPage()->find('css', $selector);
-    $element->find('css', '.contextual button')->press();
-    $element->findLink($link_locator)->click();
-
-    if ($force_visible) {
-      $this->toggleContextualTriggerVisibility($selector);
-    }
-  }
-
-  /**
-   * Toggles the visibility of a contextual trigger.
-   *
-   * @todo Remove this function when related trait added in
-   *   https://www.drupal.org/node/2821724.
-   *
-   * @param string $selector
-   *   The selector for the element that contains the contextual link.
-   */
-  protected function toggleContextualTriggerVisibility($selector) {
-    // Hovering over the element itself with should be enough, but does not
-    // work. Manually remove the visually-hidden class.
-    $this->getSession()->executeScript("jQuery('{$selector} .contextual .trigger').toggleClass('visually-hidden');");
-  }
-
   /**
    * Get themes to test.
    *
