diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 7639a1a..eb38fc3 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -32,7 +32,13 @@ parameters: count: 1 path: src/WebdriverClassicDriver.php - - message: '#^Parameter \#1 \$handle of method Facebook\\WebDriver\\Remote\\RemoteTargetLocator\:\:window\(\) expects string, mixed given\.$#' + message: '#^Parameter \#1 \$handle of method Facebook\\WebDriver\\WebDriverTargetLocator\:\:window\(\) expects string, mixed given\.$#' identifier: argument.type count: 3 path: src/WebdriverClassicDriver.php + - + # See https://github.com/php-webdriver/php-webdriver/blob/998e499b786805568deaf8cbf06f4044f05d91bf/lib/WebDriverElement.php#L43 + message: '#^Call to an undefined method Facebook\\WebDriver\\Internal\\WebDriverLocatable\&Facebook\\WebDriver\\WebDriverElement\:\:getDomProperty\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/WebdriverClassicDriver.php diff --git a/src/WebdriverClassicDriver.php b/src/WebdriverClassicDriver.php index 6918157..90c5952 100644 --- a/src/WebdriverClassicDriver.php +++ b/src/WebdriverClassicDriver.php @@ -18,14 +18,18 @@ use Facebook\WebDriver\Exception\TimeoutException; use Facebook\WebDriver\Exception\UnsupportedOperationException; use Facebook\WebDriver\Exception\WebDriverException; +use Facebook\WebDriver\Interactions\WebDriverActions; +use Facebook\WebDriver\Internal\WebDriverLocatable; +use Facebook\WebDriver\JavaScriptExecutor; use Facebook\WebDriver\Remote\DesiredCapabilities; use Facebook\WebDriver\Remote\RemoteWebDriver; -use Facebook\WebDriver\Remote\RemoteWebElement; use Facebook\WebDriver\Remote\WebDriverBrowserType; use Facebook\WebDriver\Remote\WebDriverCapabilityType; +use Facebook\WebDriver\WebDriver; use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverDimension; use Facebook\WebDriver\WebDriverElement; +use Facebook\WebDriver\WebDriverHasInputDevices; use Facebook\WebDriver\WebDriverPlatform; use Facebook\WebDriver\WebDriverRadios; use Facebook\WebDriver\WebDriverSelect; @@ -35,7 +39,9 @@ * @phpstan-type TTimeouts array{script?: null|numeric, implicit?: null|numeric, page?: null|numeric, "page load"?: null|numeric, pageLoad?: null|numeric} * @phpstan-type TCapabilities array * @phpstan-type TElementValue array|bool|mixed|string|null - * @phpstan-type TWebDriverInstantiator callable(string $driverHost, DesiredCapabilities $capabilities): RemoteWebDriver + * @phpstan-type TWebDriver WebDriver&JavaScriptExecutor&WebDriverHasInputDevices + * @phpstan-type TWebDriverElement WebDriverElement&WebDriverLocatable + * @phpstan-type TWebDriverInstantiator callable(string $driverHost, DesiredCapabilities $capabilities): TWebDriver */ class WebdriverClassicDriver extends CoreDriver { @@ -77,7 +83,10 @@ class WebdriverClassicDriver extends CoreDriver private const W3C_WINDOW_HANDLE_PREFIX = 'w3cwh:'; - private ?RemoteWebDriver $webDriver = null; + /** + * @phpstan-var TWebDriver|null + */ + private ?WebDriver $webDriver = null; private string $browserName; @@ -210,7 +219,7 @@ public function switchToWindow(?string $name = null): void public function switchToIFrame(?string $name = null): void { $frameQuery = $name; - if ($name && $this->getWebDriver()->isW3cCompliant()) { + if ($name && $this->isW3cCompliant()) { try { $frameQuery = $this->getWebDriver()->findElement(WebDriverBy::id($name)); } catch (NoSuchElementException $e) { @@ -569,14 +578,18 @@ public function doubleClick( #[Language('XPath')] string $xpath ): void { - $this->doubleClickOnElement($this->findElement($xpath)); + $element = $this->findElement($xpath); + $element->getLocationOnScreenOnceScrolledIntoView(); + $this->actions()->doubleClick($element)->perform(); } public function rightClick( #[Language('XPath')] string $xpath ): void { - $this->rightClickOnElement($this->findElement($xpath)); + $element = $this->findElement($xpath); + $element->getLocationOnScreenOnceScrolledIntoView(); + $this->actions()->contextClick($element)->perform(); } public function attachFile( @@ -601,7 +614,9 @@ public function mouseOver( #[Language('XPath')] string $xpath ): void { - $this->mouseOverElement($this->findElement($xpath)); + $element = $this->findElement($xpath); + $element->getLocationOnScreenOnceScrolledIntoView(); + $this->actions()->moveToElement($element)->perform(); } public function focus( @@ -656,7 +671,7 @@ public function dragTo( ): void { $source = $this->findElement($sourceXpath); $destination = $this->findElement($destinationXpath); - $this->getWebDriver()->action()->dragAndDrop($source, $destination)->perform(); + $this->actions()->dragAndDrop($source, $destination)->perform(); } public function executeScript( @@ -751,8 +766,8 @@ public function getBrowserName(): string */ public function getWebDriverSessionId(): ?string { - return $this->isStarted() - ? $this->getWebDriver()->getSessionID() + return $this->isStarted() && method_exists($this->getWebDriver(), 'getSessionID') + ? $this->getAsString($this->getWebDriver()->getSessionID(), 'Session ID') : null; } @@ -789,15 +804,16 @@ protected function createWebDriver(): void } /** + * @phpstan-return TWebDriver * @throws DriverException */ - protected function getWebDriver(): RemoteWebDriver + protected function getWebDriver(): WebDriver { - if ($this->webDriver) { - return $this->webDriver; + if (!$this->webDriver) { + throw new DriverException('Base driver has not been created'); } - throw new DriverException('Base driver has not been created'); + return $this->webDriver; } // @@ -886,6 +902,15 @@ private function createBrowserSpecificCapabilities(): DesiredCapabilities } } + /** + * @throws DriverException + */ + private function actions(): WebDriverActions + { + // WebDriverActions are not reset after being performed - that's why we create a new instance each time. + return new WebDriverActions($this->getWebDriver()); + } + /** * @throws DriverException */ @@ -963,11 +988,12 @@ private function executeJsOnXpath( * $this->executeJsOnElement($element, 'return argument[0].childNodes.length'); * ``` * + * @phpstan-param TWebDriverElement $element * @return mixed * @throws DriverException */ private function executeJsOnElement( - WebDriverElement $element, + $element, #[Language('JavaScript')] string $script ) { @@ -1040,37 +1066,14 @@ private function getWindowHandleFromName(string $name): string } } - private function clickOnElement(WebDriverElement $element): void - { - $element->getLocationOnScreenOnceScrolledIntoView(); - $element->click(); - } - /** + * @phpstan-param TWebDriverElement $element * @throws DriverException */ - private function doubleClickOnElement(RemoteWebElement $element): void + private function clickOnElement($element): void { $element->getLocationOnScreenOnceScrolledIntoView(); - $this->getWebDriver()->getMouse()->doubleClick($element->getCoordinates()); - } - - /** - * @throws DriverException - */ - private function rightClickOnElement(RemoteWebElement $element): void - { - $element->getLocationOnScreenOnceScrolledIntoView(); - $this->getWebDriver()->getMouse()->contextClick($element->getCoordinates()); - } - - /** - * @throws DriverException - */ - private function mouseOverElement(RemoteWebElement $element): void - { - $element->getLocationOnScreenOnceScrolledIntoView(); - $this->getWebDriver()->getMouse()->mouseMove($element->getCoordinates()); + $element->click(); } /** @@ -1100,24 +1103,28 @@ private function withWindow(?string $name, callable $callback): void } /** + * @phpstan-return TWebDriverElement * @throws DriverException */ private function findElement( #[Language('XPath')] string $xpath - ): RemoteWebElement { + ) { try { $finder = WebDriverBy::xpath($xpath); - return $this->getWebDriver()->findElement($finder); + $element = $this->getWebDriver()->findElement($finder); + assert($element instanceof WebDriverLocatable); + return $element; } catch (\Throwable $e) { throw new DriverException("Failed to find element: {$e->getMessage()}", 0, $e); } } /** + * @phpstan-param TWebDriverElement $element * @throws DriverException */ - private function selectRadioValue(WebDriverElement $element, string $value): void + private function selectRadioValue($element, string $value): void { try { (new WebDriverRadios($element))->selectByValue($value); @@ -1133,9 +1140,10 @@ private function selectRadioValue(WebDriverElement $element, string $value): voi } /** + * @phpstan-param TWebDriverElement $element * @throws DriverException */ - private function selectOptionOnElement(WebDriverElement $element, string $value, bool $multiple = false): void + private function selectOptionOnElement($element, string $value, bool $multiple = false): void { try { $select = new WebDriverSelect($element); @@ -1163,9 +1171,10 @@ private function selectOptionOnElement(WebDriverElement $element, string $value, * * Note: this implementation does not trigger a change event after deselecting the elements. * + * @phpstan-param TWebDriverElement $element * @throws DriverException */ - private function deselectAllOptions(WebDriverElement $element): void + private function deselectAllOptions($element): void { try { (new WebDriverSelect($element))->deselectAll(); @@ -1180,10 +1189,11 @@ private function deselectAllOptions(WebDriverElement $element): void } /** + * @phpstan-param TWebDriverElement $element * @throws DriverException */ private function ensureInputType( - WebDriverElement $element, + $element, #[Language('XPath')] string $xpath, string $type, @@ -1223,10 +1233,11 @@ private function jsonEncode($value, string $action, string $field): string } /** + * @phpstan-param TWebDriverElement $element * @param mixed $value * @throws DriverException */ - private function setElementDomProperty(WebDriverElement $element, string $property, $value): void + private function setElementDomProperty($element, string $property, $value): void { $this->executeJsOnElement( $element, @@ -1235,13 +1246,14 @@ private function setElementDomProperty(WebDriverElement $element, string $proper } /** + * @phpstan-param TWebDriverElement $element * @return mixed * @throws DriverException */ - private function getElementDomProperty(RemoteWebElement $element, string $property) + private function getElementDomProperty($element, string $property) { try { - return $this->getWebDriver()->isW3cCompliant() + return $this->isW3cCompliant() ? $element->getDomProperty($property) : $this->executeJsOnElement($element, "return arguments[0]['$property']"); } catch (UnsupportedOperationException $e) { @@ -1270,5 +1282,14 @@ private function getAsString($value, string $name): string return (string)$value; } + private function isW3cCompliant(): bool + { + if (!method_exists($this->getWebDriver(), 'isW3cCompliant')) { + throw new DriverException('Base driver must implement an `isW3cCompliant` method that returns a boolean.'); + } + + return (bool)$this->getWebDriver()->isW3cCompliant(); + } + // }