MDL-70148 behat: Add steps to send keys without an element
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 17 Jun 2020 06:50:04 +0000 (14:50 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 17 Nov 2020 07:14:43 +0000 (15:14 +0800)
admin/tool/behat/tests/behat/keyboard.feature [new file with mode: 0644]
lib/behat/behat_base.php
lib/tests/behat/behat_general.php

diff --git a/admin/tool/behat/tests/behat/keyboard.feature b/admin/tool/behat/tests/behat/keyboard.feature
new file mode 100644 (file)
index 0000000..ca9a74f
--- /dev/null
@@ -0,0 +1,52 @@
+@tool_behat
+Feature: Verify that keyboard steps work as expected
+  In order to use behat step definitions
+  As a test writer
+  I need to verify that the keyboard steps work as expected
+
+  @javascript
+  Scenario: Typing keys into a field causes them to be input
+    Given the following "users" exist:
+      | username | email                        | firstname | lastname | password    |
+      | saffronr | saffron.rutledge@example.com | Saffron   | Rutledge | flowerpower |
+    Given I click on "Log in" "link"
+    And I click on "Username" "field"
+    When I type "saffronr"
+    And I press the tab key
+    And I type "flowerpower"
+    And I press enter
+    Then I should see "You are logged in as Saffron Rutledge"
+
+  @javascript
+  Scenario: Using tab changes focus to the next or previous field
+    Given I click on "Log in" "link"
+    And I click on "Username" "field"
+    And the focused element is "Username" "field"
+    When I press the tab key
+    Then the focused element is "Password" "field"
+
+    And I press the shift tab key
+    And the focused element is "Username" "field"
+
+  @javascript
+  Scenario: Using the arrow keys allows me to navigate through menus
+    Given the following "users" exist:
+      | username | email                        | firstname | lastname |
+      | saffronr | saffron.rutledge@example.com | Saffron   | Rutledge |
+    And I log in as "saffronr"
+    And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element"
+    When I press the up key
+    Then the focused element is "Log out" "link"
+
+  @javascript
+  Scenario: The escape key can be used to close a dialogue
+    Given the following "course" exists:
+     | fullname  | C1|
+     | shortname | C1 |
+    And I log in as "admin"
+    And I am on "C1" course homepage
+    And I navigate to course participants
+    And I press "Enrol users"
+    And "Enrol users" "dialogue" should be visible
+    When I press the escape key
+    Then "Enrol users" "dialogue" should not be visible
index db165c7..57688c8 100644 (file)
@@ -38,6 +38,10 @@ use Behat\Mink\Session;
 require_once(__DIR__ . '/classes/component_named_selector.php');
 require_once(__DIR__ . '/classes/component_named_replacement.php');
 
 require_once(__DIR__ . '/classes/component_named_selector.php');
 require_once(__DIR__ . '/classes/component_named_replacement.php');
 
+// Alias the WebDriver\Key  class to behat_keys to make future transition to a different WebDriver implementation
+// easier.
+class_alias('WebDriver\\Key', 'behat_keys');
+
 /**
  * Steps definitions base class.
  *
 /**
  * Steps definitions base class.
  *
@@ -264,6 +268,44 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         ];
     }
 
         ];
     }
 
+    /**
+     * Send key presses straight to the currently active element.
+     *
+     * The `$keys` array contains a list of key values to send to the session as defined in the WebDriver and JsonWire
+     * specifications:
+     * - JsonWire: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#sessionsessionidkeys
+     * - W3C WebDriver: https://www.w3.org/TR/webdriver/#keyboard-actions
+     *
+     * This may be a combination of typable characters, modifier keys, and other supported keypoints.
+     *
+     * The NULL_KEY should be used to release modifier keys. If the NULL_KEY is not used then modifier keys will remain
+     * in the pressed state.
+     *
+     * Example usage:
+     *
+     *      behat_base::type_keys($this->getSession(), [behat_keys::SHIFT, behat_keys::TAB, behat_keys::NULL_KEY]);
+     *      behat_base::type_keys($this->getSession(), [behat_keys::ENTER, behat_keys::NULL_KEY]);
+     *      behat_base::type_keys($this->getSession(), [behat_keys::ESCAPE, behat_keys::NULL_KEY]);
+     *
+     * It can also be used to send text input, for example:
+     *
+     *      behat_base::type_keys(
+     *          $this->getSession(),
+     *          ['D', 'o', ' ', 'y', 'o', 'u', ' ', 'p', 'l', 'a' 'y', ' ', 'G', 'o', '?', behat_base::NULL_KEY]
+     *      );
+     *
+     *
+     * Please note: This function does not use the element/sendKeys variants but sends keys straight to the browser.
+     *
+     * @param Session $session
+     * @param string[] $keys
+     */
+    public static function type_keys(Session $session, array $keys): void {
+        $session->getDriver()->getWebDriverSession()->keys([
+            'value' => $keys,
+        ]);
+    }
+
     /**
      * Finds DOM nodes in the page using named selectors.
      *
     /**
      * Finds DOM nodes in the page using named selectors.
      *
index 49b2fdd..469fe0d 100644 (file)
 
 require_once(__DIR__ . '/../../behat/behat_base.php');
 
 
 require_once(__DIR__ . '/../../behat/behat_base.php');
 
-use Behat\Mink\Exception\ExpectationException as ExpectationException,
-    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
-    Behat\Mink\Exception\DriverException as DriverException,
-    WebDriver\Exception\NoSuchElement as NoSuchElement,
-    WebDriver\Exception\StaleElementReference as StaleElementReference,
-    Behat\Gherkin\Node\TableNode as TableNode;
+use Behat\Gherkin\Node\TableNode as TableNode;
+use Behat\Mink\Exception\DriverException as DriverException;
+use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+use Behat\Mink\Exception\ExpectationException as ExpectationException;
+use WebDriver\Exception\NoSuchElement as NoSuchElement;
+use WebDriver\Exception\StaleElementReference as StaleElementReference;
 
 /**
  * Cross component steps definitions.
 
 /**
  * Cross component steps definitions.
@@ -1710,6 +1710,143 @@ EOF;
         }
     }
 
         }
     }
 
+    /**
+     * Send key presses to the browser without first changing focusing, or applying the key presses to a specific
+     * element.
+     *
+     * Example usage of this step:
+     *     When I type "Penguin"
+     *
+     * @When    I type :keys
+     * @param   string $keys The key, or list of keys, to type
+     */
+    public function i_type(string $keys): void {
+        behat_base::type_keys($this->getSession(), str_split($keys));
+    }
+
+    /**
+     * Press a named key with an optional set of modifiers.
+     *
+     * Supported named keys are:
+     * - up
+     * - down
+     * - left
+     * - right
+     * - pageup|page_up
+     * - pagedown|page_down
+     * - home
+     * - end
+     * - insert
+     * - delete
+     * - backspace
+     * - escape
+     * - enter
+     * - tab
+     *
+     * Supported moderators are:
+     * - shift
+     * - ctrl
+     * - alt
+     * - meta
+     *
+     * Example usage of this new step:
+     *     When I press the up key
+     *     When I press the space key
+     *     When I press the shift tab key
+     *
+     * Multiple moderator keys can be combined using the '+' operator, for example:
+     *     When I press the ctrl+shift enter key
+     *     When I press the ctrl + shift enter key
+     *
+     * @When    /^I press the (?P<modifiers_string>.* )?(?P<key_string>.*) key$/
+     * @param   string $modifiers A list of keyboard modifiers, separated by the `+` character
+     * @param   string $key The name of the key to press
+     */
+    public function i_press_named_key(string $modifiers, string $key): void {
+        behat_base::require_javascript_in_session($this->getSession());
+
+        $keys = [];
+
+        foreach (explode('+', $modifiers) as $modifier) {
+            switch (strtoupper(trim($modifier))) {
+                case '':
+                    break;
+                case 'SHIFT':
+                    $keys[] = behat_keys::SHIFT;
+                    break;
+                case 'CTRL':
+                    $keys[] = behat_keys::CONTROL;
+                    break;
+                case 'ALT':
+                    $keys[] = behat_keys::ALT;
+                    break;
+                case 'META':
+                    $keys[] = behat_keys::META;
+                    break;
+                default:
+                    throw new \coding_exception("Unknown modifier key '$modifier'}");
+            }
+        }
+
+        $modifier = trim($key);
+        switch (strtoupper($key)) {
+            case 'UP':
+                $keys[] = behat_keys::UP_ARROW;
+                break;
+            case 'DOWN':
+                $keys[] = behat_keys::DOWN_ARROW;
+                break;
+            case 'LEFT':
+                $keys[] = behat_keys::LEFT_ARROW;
+                break;
+            case 'RIGHT':
+                $keys[] = behat_keys::RIGHT_ARROW;
+                break;
+            case 'HOME':
+                $keys[] = behat_keys::HOME;
+                break;
+            case 'END':
+                $keys[] = behat_keys::END;
+                break;
+            case 'INSERT':
+                $keys[] = behat_keys::INSERT;
+                break;
+            case 'BACKSPACE':
+                $keys[] = behat_keys::BACKSPACE;
+                break;
+            case 'DELETE':
+                $keys[] = behat_keys::DELETE;
+                break;
+            case 'PAGEUP':
+            case 'PAGE_UP':
+                $keys[] = behat_keys::PAGE_UP;
+                break;
+            case 'PAGEDOWN':
+            case 'PAGE_DOWN':
+                $keys[] = behat_keys::PAGE_DOWN;
+                break;
+            case 'ESCAPE':
+                $keys[] = behat_keys::ESCAPE;
+                break;
+            case 'ENTER':
+                $keys[] = behat_keys::ENTER;
+                break;
+            case 'TAB':
+                $keys[] = behat_keys::TAB;
+                break;
+            case 'SPACE':
+                $keys[] = behat_keys::SPACE;
+                break;
+            default:
+                throw new \coding_exception("Unknown key '$key'}");
+        }
+
+        // Always send the NULL key as the last key.
+        $keys[] = behat_keys::NULL_KEY;
+
+        behat_base::type_keys($this->getSession(), $keys);
+    }
+
     /**
      * Trigger a keydown event for a key on a specific element.
      *
     /**
      * Trigger a keydown event for a key on a specific element.
      *