diff --git a/composer.json b/composer.json index e8f1a03..f7cae58 100644 --- a/composer.json +++ b/composer.json @@ -20,8 +20,8 @@ }, "require-dev": { "mink/driver-testsuite": "dev-master", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan": "^2", + "phpstan/phpstan-phpunit": "^2", "phpunit/phpunit": "^9.6.8", "symfony/error-handler": "^5.4 || ^6.0 || ^7.0", "symfony/process": "^5.4 || ^6.0 || ^7.0", diff --git a/phpstan.dist.neon b/phpstan.dist.neon index db774f4..5825c4b 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,10 +1,12 @@ parameters: - level: 8 + level: 10 paths: - src - tests - checkMissingIterableValueType: false treatPhpDocTypesAsCertain: false + stubFiles: + - stubs/WebDriverTimeouts.stub + - stubs/RemoteWebDriver.stub includes: - vendor/phpstan/phpstan-phpunit/extension.neon diff --git a/src/WebdriverClassicDriver.php b/src/WebdriverClassicDriver.php index 438abd5..a7312e2 100644 --- a/src/WebdriverClassicDriver.php +++ b/src/WebdriverClassicDriver.php @@ -77,6 +77,9 @@ class WebdriverClassicDriver extends CoreDriver private DesiredCapabilities $desiredCapabilities; + /** + * @var array{script?: null|numeric, implicit?: null|numeric, page?: null|numeric} + */ private array $timeouts = []; private string $webDriverHost; @@ -85,6 +88,7 @@ class WebdriverClassicDriver extends CoreDriver /** * @param string $browserName One of 'edge', 'firefox', 'chrome' or any one of {@see WebDriverBrowserType} constants. + * @param array $desiredCapabilities */ public function __construct( string $browserName = self::DEFAULT_BROWSER, @@ -262,7 +266,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(); @@ -299,7 +303,7 @@ public function getText( return str_replace( ["\r\n", "\r", "\n"], ' ', - $this->getElementDomProperty($this->findElement($xpath), 'innerText') + $this->getAsString($this->getElementDomProperty($this->findElement($xpath), 'innerText'), 'The element\'s innerText') ); } @@ -307,14 +311,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( @@ -326,9 +330,13 @@ 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"); } + /** + * @return array|bool|mixed|string|null + */ public function getValue( #[Language('XPath')] string $xpath @@ -369,6 +377,9 @@ public function getValue( } } + /** + * @param array|bool|mixed|string|null $value + */ public function setValue( #[Language('XPath')] string $xpath, @@ -386,7 +397,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; } @@ -736,7 +747,7 @@ public function getWebDriverSessionId(): ?string /** * Sets the timeouts to apply to the webdriver session * - * @param array $timeouts The session timeout settings: Array of {script, implicit, page} => time in milliseconds + * @param array{script?: numeric, implicit?: numeric, page?: numeric} $timeouts The session timeout settings: Array of {script, implicit, page} => time in milliseconds * @throws DriverException * @api */ @@ -785,6 +796,8 @@ private function getNormalisedBrowserName(): string /** * Detect and assign appropriate browser capabilities * + * @param array $desiredCapabilities + * * @see https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities */ private function initCapabilities(array $desiredCapabilities): DesiredCapabilities @@ -1227,5 +1240,20 @@ private function getElementDomProperty(RemoteWebElement $element, string $proper } } + /** + * @param mixed $value + */ + private function getAsString($value, string $name): string + { + if (!is_scalar($value)) { + $actualType = gettype($value); + throw new \RuntimeException( + "$name should be a string or at least a scalar value, but received `$actualType` instead" + ); + } + + return (string)$value; + } + // } diff --git a/stubs/RemoteWebDriver.stub b/stubs/RemoteWebDriver.stub new file mode 100644 index 0000000..47d0549 --- /dev/null +++ b/stubs/RemoteWebDriver.stub @@ -0,0 +1,11 @@ + + */ + public function getWindowHandles(): array; +} diff --git a/stubs/WebDriverTimeouts.stub b/stubs/WebDriverTimeouts.stub new file mode 100644 index 0000000..6513487 --- /dev/null +++ b/stubs/WebDriverTimeouts.stub @@ -0,0 +1,24 @@ +driver->visit($this->pathTo('/page_load.php?sleep=2')); } + /** + * @return iterable + */ public static function deprecatedPageLoadDataProvider(): iterable { yield 'selenium 3 style' => ['type' => 'pageLoad']; diff --git a/tests/WebdriverClassicConfig.php b/tests/WebdriverClassicConfig.php index f3d19bd..3dbd06d 100644 --- a/tests/WebdriverClassicConfig.php +++ b/tests/WebdriverClassicConfig.php @@ -21,7 +21,10 @@ public static function getInstance(): self public function createDriver(): WebdriverClassicDriver { - $seleniumHost = $_SERVER['DRIVER_URL']; + $seleniumHost = $_SERVER['DRIVER_URL'] ?? null; + if (!is_string($seleniumHost)) { + throw new \RuntimeException('Selenium host must be specified (as a string) in $_SERVER[DRIVER_URL].'); + } return new WebdriverClassicDriver($this->getBrowserName(), [], $seleniumHost); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d4d3a01..dd63285 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -4,9 +4,11 @@ use Symfony\Component\Process\Process; -$minkTestServerPort = isset($_SERVER['WEB_FIXTURES_HOST']) - ? parse_url($_SERVER['WEB_FIXTURES_HOST'], PHP_URL_PORT) - : '8002'; +$fixturesHost = $_SERVER['WEB_FIXTURES_HOST'] ?? '//host:8002'; +if (!is_string($fixturesHost)) { + throw new RuntimeException('The fixtures host must be specified in $_SERVER[WEB_FIXTURES_HOST] as a string'); +} +$minkTestServerPort = parse_url($fixturesHost, PHP_URL_PORT); $minkTestServer = new Process([ PHP_BINARY,