1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\Behat;
6:
7: use Atk4\Core\WarnDynamicPropertyTrait;
8: use WebDriver\Element as WebDriverElement;
9:
10: class MinkSeleniumDriver extends \Behat\Mink\Driver\Selenium2Driver
11: {
12: use WarnDynamicPropertyTrait;
13:
14: public function __construct(\Behat\Mink\Driver\Selenium2Driver $driver) // @phpstan-ignore-line
15: {
16: $class = self::class;
17: while (($class = get_parent_class($class)) !== false) {
18: \Closure::bind(function () use ($driver) {
19: foreach (get_object_vars($driver) as $k => $v) {
20: $this->{$k} = $v;
21: }
22: }, $this, $class)();
23: }
24: }
25:
26: #[\Override]
27: public function getText($xpath): string
28: {
29: // HTMLElement::innerText returns rendered text as when copied to the clipboard
30: // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText
31: // https://github.com/minkphp/MinkSelenium2Driver/pull/327
32: // https://github.com/minkphp/MinkSelenium2Driver/pull/328
33: return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.innerText;');
34: }
35:
36: protected function findElement(string $xpath): WebDriverElement
37: {
38: return \Closure::bind(function () use ($xpath) {
39: return $this->findElement($xpath);
40: }, $this, parent::class)();
41: }
42:
43: protected function clickOnElement(WebDriverElement $element): void
44: {
45: \Closure::bind(function () use ($element) {
46: $this->clickOnElement($element);
47: }, $this, parent::class)();
48: }
49:
50: #[\Override]
51: protected function mouseOverElement(WebDriverElement $element): void
52: {
53: // move the element into the viewport
54: // needed at least for Firefox as Selenium moveto does move the mouse cursor only
55: $this->executeScript('arguments[0].scrollIntoView({ behaviour: \'instant\', block: \'center\', inline: \'center\' })', [$element]);
56:
57: $this->getWebDriverSession()->moveto(['element' => $element->getID()]);
58: }
59:
60: private function executeJsSelectText(WebDriverElement $element, int $start, int $stop = null): void
61: {
62: $this->executeScript(
63: 'arguments[0].setSelectionRange(Math.min(arguments[1], Number.MAX_SAFE_INTEGER), Math.min(arguments[2], Number.MAX_SAFE_INTEGER));',
64: [$element, $start, $stop ?? $start]
65: );
66: }
67:
68: /**
69: * @param 'type' $action
70: * @param string $options
71: */
72: protected function executeSynJsAndWait(string $action, WebDriverElement $element, $options): void
73: {
74: $this->withSyn();
75:
76: $waitUniqueKey = '__wait__' . hash('sha256', microtime(true) . random_bytes(64));
77: $this->executeScript(
78: 'window.syn[arguments[2]] = true; window.syn.' . $action . '(arguments[0], arguments[1], () => delete window.syn[arguments[2]]);',
79: [$element, $options, $waitUniqueKey]
80: );
81: $this->wait(5000, 'typeof window.syn[arguments[0]] === \'undefined\'', [$waitUniqueKey]);
82: }
83:
84: /**
85: * @param string $text special characters can be passed like "[shift]T[shift-up]eest[left][left][backspace]"
86: */
87: public function keyboardWrite(string $xpath, $text): void
88: {
89: $element = $this->findElement($xpath);
90:
91: $focusedElement = $this->getWebDriverSession()->activeElement();
92: if ($element->getID() !== $focusedElement->getID()) {
93: $this->clickOnElement($element);
94: $focusedElement = $this->getWebDriverSession()->activeElement();
95: }
96:
97: if (in_array($focusedElement->name(), ['input', 'textarea'], true)) {
98: $this->executeJsSelectText($focusedElement, \PHP_INT_MAX);
99: }
100:
101: $this->executeSynJsAndWait('type', $element, $text);
102: }
103: }
104: