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 0000000..bbf5612
--- /dev/null
+++ b/core/modules/contextual/tests/modules/contextual_test/contextual_test.info.yml
@@ -0,0 +1,9 @@
+name: 'Contextual Test'
+type: module
+description: 'Provides test contextual links.'
+package: Core
+version: VERSION
+core: 8.x
+dependencies:
+  - router_test
+  - 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 0000000..35e03b7
--- /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 0000000..b8f2e60
--- /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 0000000..78ca61d
--- /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 0000000..099f00c
--- /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 0000000..dc1afc3
--- /dev/null
+++ b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinkClickTrait.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Tests\contextual\FunctionalJavascript;
+
+use Behat\Mink\Element\NodeElement;
+
+/**
+ * Functions for testing contextual links.
+ */
+trait ContextualLinkClickTrait {
+
+  /**
+   * Clicks a contextual link.
+   *
+   * @param \Behat\Mink\Element\NodeElement $element
+   *   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(NodeElement $element, $link_locator, $force_visible = TRUE) {
+    $button = $element->find('css', '.contextual button');
+    $find_button_script = "jQuery('#{$element->getAttribute('id')} .contextual button')";
+    if ($force_visible) {
+      // The button to show contextual links is only visible when in focus.
+      // Force button to be visible.
+      $this->getSession()->executeScript($find_button_script . '.removeClass("visually-hidden");');
+    }
+    $button->press();
+    $link = $element->findLink($link_locator);
+    $link->click();
+    if ($force_visible) {
+      // Hide button again if needed.
+      $this->getSession()->executeScript($find_button_script . '.addClass("visually-hidden");');
+    }
+  }
+
+}
diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php
new file mode 100644
index 0000000..d3b10bc
--- /dev/null
+++ b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tests\contextual\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+use Drupal\user\Entity\Role;
+
+/**
+ * Tests clicking contextual links.
+ *
+ * @group contextual
+ */
+class ContextualLinksTest extends JavascriptTestBase {
+
+  use ContextualLinkClickTrait;
+
+  protected static $modules = ['block', 'contextual', 'contextual_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->drupalLogin($this->createUser(
+      [
+        'access contextual links',
+      ]
+    ));
+    $this->placeBlock('system_powered_by_block', ['id' => 'powered']);
+  }
+
+  /**
+   * Test clicking contextual links.
+   */
+  public function testContextualLinks() {
+    // Test clicking contextual link without toolbar.
+    $this->drupalGet('user');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $page = $this->getSession()->getPage();
+    $this->clickContextualLink($page->find('css', '#block-powered'), '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.
+    $page->find('css', '.contextual-toolbar-tab button')->press();
+    $this->clickContextualLink($page->find('css', '#block-powered'), 'Test Link', FALSE);
+    $this->assertSession()->pageTextContains('Everything is contextual!');
+  }
+
+}
