-
-
Notifications
You must be signed in to change notification settings - Fork 13
/
KeyboardTrait.php
164 lines (145 loc) · 5.53 KB
/
KeyboardTrait.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<?php
declare(strict_types=1);
namespace DrevOps\BehatSteps;
use Behat\Mink\Driver\Selenium2Driver;
use Behat\Mink\Exception\UnsupportedDriverActionException;
/**
* Trait KeyboardTrait.
*
* Behat trait for keyboard interactions.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
trait KeyboardTrait {
/**
* Press multiple keyboard keys, optionally on element.
*
* @Given I press the :keys keys
* @Given I press the :keys keys on :selector
*/
public function keyboardPressKeysOnElement(string $keys, ?string $selector = NULL): void {
foreach (str_split($keys) as $char) {
$this->keyboardPressKeyOnElement($char, $selector);
}
}
/**
* Press keyboard key, optionally on element.
*
* @param string $char
* Character or one of the pre-defined special keyboard keys.
* @param string $selector
* Optional CSS selector for an element to trigger the key on. If omitted,
* the key will be triggered on the 'html' element of the page.
*
* @throws \Behat\Mink\Exception\UnsupportedDriverActionException
* If method is used for invalid driver.
*
* @Given I press the :char key
* @Given I press the :char key on :selector
*/
public function keyboardPressKeyOnElement(string $char, ?string $selector = NULL): void {
$driver = $this->getSession()->getDriver();
if (!$driver instanceof Selenium2Driver) {
throw new UnsupportedDriverActionException('Method can be used only with Selenium2 driver', $driver);
}
$keys = [
'backspace' => "\b",
'tab' => "\t",
'enter' => "\r",
'shift' => 'shift',
'ctrl' => 'ctrl',
'alt' => 'alt',
'pause' => 'pause',
'break' => 'break',
'escape' => 'escape',
'esc' => 'escape',
'end' => 'end',
'home' => 'home',
'left' => 'left',
'up' => 'up',
'right' => 'right',
'down' => 'down',
'insert' => 'insert',
'delete' => 'delete',
'pageup' => 'page-up',
'page-up' => 'page-up',
'pagedown' => 'page-down',
'page-down' => 'page-down',
'capslock' => 'caps',
'caps' => 'caps',
];
// Convert provided character sequence to special keys.
if (is_string($char)) {
if (strlen($char) < 1) {
throw new \Exception('keyPress($char) was invoked but the $char parameter was empty.');
}
// Consider provided characters string longer then 1 to be a keyboard key.
elseif (strlen($char) > 1) {
if (!array_key_exists(strtolower($char), $keys)) {
throw new \Exception('Unsupported key name provided');
}
// Special case for tab key triggered in window without target element
// focused: Syn (JS library that provides synthetic events) can tab only
// from another element that can receive focus, so we inject such
// element as a very first element after opening <body> tag. This
// element is visually hidden, but compatible with screen readers. Then
// we trigger key on this element to make sure that an element that
// supposed to get the very first focus from tab index actually gets it.
// Note that injecting element and triggering key press on it does not
// make it focused itself.
if (is_null($selector) && $char === 'tab') {
$selector = '#injected-focusable';
$script = <<<JS
(function() {
if (document.querySelectorAll('body #injected-focusable').length === 0) {
document.querySelector('body').insertAdjacentHTML('afterbegin', '<a id="injected-focusable" style="position: absolute;width: 1px;height: 1px;margin: -1px;padding: 0;overflow: hidden;clip: rect(0,0,0,0);border: 0;"></a>');
}
})();
JS;
$this->getSession()->getDriver()->evaluateScript($script);
}
$char = $keys[strtolower($char)];
}
}
$selector = $selector ?: 'html';
// Element to trigger key press on.
$element = $this->getSession()->getPage()->find('css', $selector);
if (!$element) {
throw new \RuntimeException(sprintf('Unable to find an element with "%s" selector.', $selector));
}
$this->keyboardTriggerKey($element->getXpath(), $char);
}
/**
* Trigger key on the element.
*
* Uses Syn library injected by original Selenium2 class to trigger browser
* events.
*
* @param string $xpath
* XPath string for an element to trigger the key on.
* @param string $key
* Key to trigger. Special key values must be provided as strings (i.e.
* 'tab' key as "\t", 'enter' key as "\r" etc.).
*
* @throws \Behat\Mink\Exception\UnsupportedDriverActionException
* If method is used for invalid driver.
*/
protected function keyboardTriggerKey(string $xpath, string $key): void {
$driver = $this->getSession()->getDriver();
if (!$driver instanceof Selenium2Driver) {
throw new UnsupportedDriverActionException('Method can be used only with Selenium2 driver', $driver);
}
// Use reflection to re-use Syn library injection and execution of JS on
// element.
$reflector = new \ReflectionClass($driver);
$withSynReflection = $reflector->getMethod('withSyn');
$withSynReflection->setAccessible(TRUE);
$executeJsOnXpathReflection = $reflector->getMethod('executeJsOnXpath');
$executeJsOnXpathReflection->setAccessible(TRUE);
$withSynResult = $withSynReflection->invoke($driver);
$executeJsOnXpathReflection->invokeArgs($withSynResult, [
$xpath,
sprintf("syn.key({{ELEMENT}}, '%s');", $key),
]);
}
}