diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 7b42960..f0dc8e8 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -3,7 +3,7 @@ includes: - vendor/phpstan/phpstan-phpunit/rules.neon parameters: - level: 8 + level: 9 paths: - src - tests diff --git a/src/WebdriverClassicDriver.php b/src/WebdriverClassicDriver.php index da24eae..47c18f7 100644 --- a/src/WebdriverClassicDriver.php +++ b/src/WebdriverClassicDriver.php @@ -280,7 +280,7 @@ public function getWindowNames(): array public function getWindowName(): string { - $name = (string)$this->evaluateScript('window.name'); + $name = $this->getAsString($this->evaluateScript('window.name'), 'Window name'); if ($name === '') { $name = self::W3C_WINDOW_HANDLE_PREFIX . $this->getWebDriver()->getWindowHandle(); @@ -317,7 +317,7 @@ public function getText( return trim(str_replace( ["\r\n", "\r", "\n", "\xc2\xa0"], ' ', - $this->getElementDomProperty($this->findElement($xpath), 'innerText') + $this->getAsString($this->getElementDomProperty($this->findElement($xpath), 'innerText'), 'The element\'s innerText') )); } @@ -325,14 +325,14 @@ public function getHtml( #[Language('XPath')] string $xpath ): string { - return $this->getElementDomProperty($this->findElement($xpath), 'innerHTML'); + return $this->getAsString($this->getElementDomProperty($this->findElement($xpath), 'innerHTML'), 'The element\'s innerHTML'); } public function getOuterHtml( #[Language('XPath')] string $xpath ): string { - return $this->getElementDomProperty($this->findElement($xpath), 'outerHTML'); + return $this->getAsString($this->getElementDomProperty($this->findElement($xpath), 'outerHTML'), 'The element\'s outerHTML'); } public function getAttribute( @@ -344,7 +344,8 @@ public function getAttribute( // so we cannot use webdriver api for this. See also: https://w3c.github.io/webdriver/#dfn-get-element-attribute $escapedName = $this->jsonEncode($name, 'get attribute', 'attribute name'); $script = "return arguments[0].getAttribute($escapedName)"; - return $this->executeJsOnXpath($xpath, $script); + $result = $this->executeJsOnXpath($xpath, $script); + return $result === null ? null : $this->getAsString($result, "The element's $name attribute"); } /** @@ -412,7 +413,7 @@ public function setValue( if (is_array($value)) { $this->deselectAllOptions($element); foreach ($value as $option) { - $this->selectOptionOnElement($element, $option, true); + $this->selectOptionOnElement($element, $this->getAsString($option, 'Option value'), true); } return; } @@ -941,12 +942,13 @@ private function charToSynOptions($char, ?string $modifier = null): string * Executes JS on a given element - pass in a js script string and argument[0] will * be replaced with a reference to the result of the $xpath query * - * @param string $xpath the xpath to search with - * @param string $script the script to execute + * Example: + * ``` + * $this->executeJsOnXpath($xpath, 'return argument[0].childNodes.length'); + * ``` * * @return mixed * @throws DriverException - * @example $this->executeJsOnXpath($xpath, 'return argument[0].childNodes.length'); */ private function executeJsOnXpath( #[Language('XPath')] @@ -960,11 +962,13 @@ private function executeJsOnXpath( /** * Executes JS on a given element - pass in a js script string and argument[0] will contain a reference to the element * - * @param RemoteWebElement $element the webdriver element - * @param string $script the script to execute + * Example: + * ``` + * $this->executeJsOnElement($element, 'return argument[0].childNodes.length'); + * ``` + * * @return mixed * @throws DriverException - * @example $this->executeJsOnXpath($xpath, 'return argument[0].childNodes.length'); */ private function executeJsOnElement( RemoteWebElement $element, @@ -1257,5 +1261,21 @@ private function getElementDomProperty(RemoteWebElement $element, string $proper } } + /** + * @param mixed $value + * @throws DriverException + */ + private function getAsString($value, string $name): string + { + if (!is_scalar($value)) { + $actualType = gettype($value); + throw new DriverException( + "$name should be a string or at least a scalar value, but received `$actualType` instead" + ); + } + + return (string)$value; + } + // } diff --git a/tests/Custom/WebDriverTest.php b/tests/Custom/WebDriverTest.php index 6a778d4..c56eba5 100644 --- a/tests/Custom/WebDriverTest.php +++ b/tests/Custom/WebDriverTest.php @@ -3,6 +3,7 @@ namespace Mink\WebdriverClassicDriver\Tests\Custom; use Behat\Mink\Exception\DriverException; +use Facebook\WebDriver\Remote\RemoteWebElement; use Mink\WebdriverClassicDriver\Tests\WebDriverMockingTrait; use Mink\WebdriverClassicDriver\WebdriverClassicDriver; @@ -69,4 +70,28 @@ public function testClassicDriverCanProvideBrowserName(): void $this->driver->getBrowserName() ); } + + public function testThatDriverCatchesUnexpectedAttributeValueType(): void + { + $mockWebDriver = $this->createMockWebDriver(); + $mockElement = $this->createMock(RemoteWebElement::class); + $mockWebDriver + ->expects($this->once()) + ->method('findElement') + ->willReturn($mockElement); + $mockWebDriver + ->expects($this->once()) + ->method('executeScript') + ->with('return arguments[0].getAttribute("some-attribute")', [$mockElement]) + ->willReturn(['invalid attribute value']); + + $driver = new WebdriverClassicDriver('fake browser', [], 'example.com', fn() => $mockWebDriver); + + $driver->start(); + + $this->expectException(DriverException::class); + $this->expectExceptionMessage('The element\'s some-attribute attribute should be a string or at least a scalar value, but received `array` instead'); + + $driver->getAttribute('//fake', 'some-attribute'); + } }