From 085c55430c5e17c1b710ecdc9bd22e175dffed4b Mon Sep 17 00:00:00 2001 From: Tavo Nieves J <64917965+TavoNiievez@users.noreply.github.com> Date: Mon, 29 Nov 2021 21:48:27 -0500 Subject: [PATCH] Update codebase to PHP 7.4 (#82) --- composer.json | 6 +- readme.md | 6 +- .../Exception/ConnectionException.php | 7 +- .../Lib/Interfaces/ScreenshotSaver.php | 4 +- .../Lib/Interfaces/SessionSnapshot.php | 11 +- src/Codeception/Module/WebDriver.php | 903 +++++++++--------- .../Constraints/WebDriverConstraintTest.php | 12 +- .../WebDriverNotConstraintTest.php | 11 +- .../Codeception/Module/TestsForBrowsers.php | 6 +- tests/unit/Codeception/Module/TestsForWeb.php | 50 +- tests/web/WebDriverTest.php | 72 +- 11 files changed, 555 insertions(+), 533 deletions(-) diff --git a/composer.json b/composer.json index b70c2fb6..df45450d 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name":"codeception/module-webdriver", "description":"WebDriver module for Codeception", "keywords":["codeception", "browser-testing", "acceptance-testing"], - "homepage":"http://codeception.com/", + "homepage":"https://codeception.com/", "type":"library", "license":"MIT", "authors":[ @@ -19,7 +19,9 @@ "minimum-stability": "RC", "require": { - "php": ">=5.6.0 <9.0", + "php": "^7.4 | ^8.0", + "ext-json": "*", + "ext-mbstring": "*", "codeception/codeception": "^4.0", "php-webdriver/webdriver": "^1.8.0" }, diff --git a/readme.md b/readme.md index 1ff2a2e5..50801ca2 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,6 @@ -# WebDriver module for Codeception +# Codeception Module WebDriver + +A WebDriver module for Codeception. [![Chrome Tests](https://github.com/Codeception/module-webdriver/actions/workflows/webdriver-chrome.yml/badge.svg)](https://github.com/Codeception/module-webdriver/actions/workflows/webdriver-chrome.yml) [![Chrome Headless Tests](https://github.com/Codeception/module-webdriver/actions/workflows/webdriver-chrome-headless.yml/badge.svg)](https://github.com/Codeception/module-webdriver/actions/workflows/webdriver-chrome-headless.yml) @@ -7,7 +9,7 @@ ## Installation ``` -composer require --dev "codeception/module-webdriver" +composer require "codeception/module-webdriver" --dev ``` ## Documentation diff --git a/src/Codeception/Exception/ConnectionException.php b/src/Codeception/Exception/ConnectionException.php index f0a81e9f..62e2d955 100644 --- a/src/Codeception/Exception/ConnectionException.php +++ b/src/Codeception/Exception/ConnectionException.php @@ -1,6 +1,11 @@ getModule('{{MODULE_NAME}}')->_saveScreenshot(codecept_output_dir().'screenshot_1.png'); * ``` * @api - * @param $filename */ - public function _saveScreenshot($filename); + public function _saveScreenshot(string $filename); } diff --git a/src/Codeception/Lib/Interfaces/SessionSnapshot.php b/src/Codeception/Lib/Interfaces/SessionSnapshot.php index 00ee66a8..74617d95 100644 --- a/src/Codeception/Lib/Interfaces/SessionSnapshot.php +++ b/src/Codeception/Lib/Interfaces/SessionSnapshot.php @@ -1,4 +1,5 @@ saveSessionSnapshot('login'); * } - * ?> * ``` * - * @param $name * @return mixed */ - public function saveSessionSnapshot($name); + public function saveSessionSnapshot(string $name); /** * Loads cookies from a saved snapshot. @@ -41,18 +40,16 @@ public function saveSessionSnapshot($name); * * See [saveSessionSnapshot](#saveSessionSnapshot) * - * @param $name * @return mixed */ - public function loadSessionSnapshot($name); + public function loadSessionSnapshot(string $name); /** * Deletes session snapshot. * * See [saveSessionSnapshot](#saveSessionSnapshot) * - * @param $name * @return mixed */ - public function deleteSessionSnapshot($name); + public function deleteSessionSnapshot(string $name); } diff --git a/src/Codeception/Module/WebDriver.php b/src/Codeception/Module/WebDriver.php index eaff6c1f..01a742a3 100644 --- a/src/Codeception/Module/WebDriver.php +++ b/src/Codeception/Module/WebDriver.php @@ -1,7 +1,10 @@ 'http', 'host' => '127.0.0.1', @@ -401,40 +414,61 @@ class WebDriver extends CodeceptionModule implements 'webdriver_proxy_port' => null, ]; - protected $wdHost; + protected ?string $wdHost = null; + + /** + * @var mixed + */ protected $capabilities; + + /** + * @var float|int|null + */ protected $connectionTimeoutInMs; + + /** + * @var float|int|null + */ protected $requestTimeoutInMs; + protected $test; - protected $sessions = []; - protected $sessionSnapshots = []; + + protected array $sessions = []; + + protected array $sessionSnapshots = []; + + /** + * @var mixed + */ protected $webdriverProxy; - protected $webdriverProxyPort; /** - * @var RemoteWebDriver + * @var mixed */ - public $webDriver; + protected $webdriverProxyPort; + + public ?RemoteWebDriver $webDriver = null; /** - * @var RemoteWebElement + * @var RemoteWebDriver|RemoteWebElement */ - protected $baseElement; + protected $baseElement = null; - public function _requires() + public function _requires(): array { - return ['Facebook\WebDriver\Remote\RemoteWebDriver' => '"php-webdriver/webdriver": "^1.0.1"']; + return [RemoteWebDriver::class => '"php-webdriver/webdriver": "^1.0.1"']; } /** - * @return RemoteWebElement * @throws ModuleException + * @return RemoteWebDriver|RemoteWebElement|WebDriverSearchContext */ protected function getBaseElement() { if (!$this->baseElement) { throw new ModuleException($this, "Page not loaded. Use `\$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it"); } + return $this->baseElement; } @@ -446,6 +480,7 @@ public function _initialize() if ($proxy = $this->getProxy()) { $this->capabilities[WebDriverCapabilityType::PROXY] = $proxy; } + $this->connectionTimeoutInMs = $this->config['connection_timeout'] * 1000; $this->requestTimeoutInMs = $this->config['request_timeout'] * 1000; $this->webdriverProxy = $this->config['webdriver_proxy']; @@ -495,23 +530,23 @@ public function _initialize() * In this case, please ensure that `\Helper\Acceptance` is loaded before WebDriver so new capabilities could be applied. * * @api - * @param \Closure $capabilityFunction */ - public function _capabilities(\Closure $capabilityFunction) + public function _capabilities(Closure $capabilityFunction): void { $this->capabilities = $capabilityFunction($this->capabilities); } - public function _conflicts() + public function _conflicts(): string { - return 'Codeception\Lib\Interfaces\Web'; + return WebInterface::class; } public function _before(TestInterface $test) { - if (!isset($this->webDriver) && $this->config['start']) { + if ($this->webDriver === null && $this->config['start']) { $this->_initializeSession(); } + $this->setBaseElement(); $test->getMetadata()->setCurrent( @@ -533,15 +568,15 @@ public function _before(TestInterface $test) * $this->getModule('WebDriver')->_restart(['browser' => $browser]); // reconfigure + restart * ``` * - * @param array $config * @api */ - public function _restart($config = []) + public function _restart(array $config = []): void { $this->webDriver->quit(); if (!empty($config)) { $this->_reconfigure($config); } + $this->_initializeSession(); } @@ -550,32 +585,34 @@ protected function onReconfigure() $this->_initialize(); } - protected function loadFirefoxProfile() + protected function loadFirefoxProfile(): void { if (!array_key_exists('firefox_profile', $this->config['capabilities'])) { return; } $firefox_profile = $this->config['capabilities']['firefox_profile']; - if (file_exists($firefox_profile) === false) { + if (!file_exists($firefox_profile)) { throw new ModuleConfigException( __CLASS__, "Firefox profile does not exist under given path " . $firefox_profile ); } + // Set firefox profile as capability $this->capabilities['firefox_profile'] = file_get_contents($firefox_profile); } - protected function initialWindowSize() + protected function initialWindowSize(): void { if ($this->config['window_size'] == 'maximize') { $this->maximizeWindow(); return; } - $size = explode('x', $this->config['window_size']); + + $size = explode('x', (string) $this->config['window_size']); if (count($size) == 2) { - $this->resizeWindow(intval($size[0]), intval($size[1])); + $this->resizeWindow((int) $size[0], (int) $size[1]); } } @@ -585,12 +622,13 @@ public function _after(TestInterface $test) $this->stopAllSessions(); return; } - if ($this->config['clear_cookies'] && isset($this->webDriver)) { + + if ($this->config['clear_cookies'] && $this->webDriver !== null) { try { $this->webDriver->manage()->deleteAllCookies(); - } catch (\Exception $e) { + } catch (Exception $exception) { // may cause fatal errors when not handled - $this->debug("Error, can't clean cookies after a test: " . $e->getMessage()); + $this->debug("Error, can't clean cookies after a test: " . $exception->getMessage()); } } } @@ -598,26 +636,25 @@ public function _after(TestInterface $test) public function _failed(TestInterface $test, $fail) { $this->debugWebDriverLogs($test); - $filename = preg_replace('~[^a-zA-Z0-9\x80-\xff]~', '.', Descriptor::getTestSignatureUnique($test)); + $filename = preg_replace('#[^a-zA-Z0-9\x80-\xff]#', '.', Descriptor::getTestSignatureUnique($test)); $outputDir = codecept_output_dir(); $this->_saveScreenshot($report = $outputDir . mb_strcut($filename, 0, 245, 'utf-8') . '.fail.png'); $test->getMetadata()->addReport('png', $report); $this->_savePageSource($report = $outputDir . mb_strcut($filename, 0, 244, 'utf-8') . '.fail.html'); $test->getMetadata()->addReport('html', $report); - $this->debug("Screenshot and page source were saved into '$outputDir' dir"); + $this->debug("Screenshot and page source were saved into '{$outputDir}' dir"); } /** * Print out latest Selenium Logs in debug mode - * - * @param \Codeception\TestInterface $test */ - public function debugWebDriverLogs(TestInterface $test = null) + public function debugWebDriverLogs(TestInterface $test = null): void { - if (!isset($this->webDriver)) { + if ($this->webDriver === null) { $this->debug('WebDriver::debugWebDriverLogs method has been called when webDriver is not set'); return; } + // don't show logs if log entries not set if (!$this->config['debug_log_entries']) { return; @@ -636,6 +673,7 @@ public function debugWebDriverLogs(TestInterface $test = null) $this->debugSection("Selenium {$logType} Logs", " EMPTY "); continue; } + $this->debugSection("Selenium {$logType} Logs", "\n" . $this->formatLogEntries($logEntries)); if ($logType === 'browser' && $this->config['log_js_errors'] @@ -644,7 +682,7 @@ public function debugWebDriverLogs(TestInterface $test = null) $this->logJSErrors($test, $logEntries); } } - } catch (\Exception $e) { + } catch (Exception $e) { $this->debug('Unable to retrieve Selenium logs : ' . $e->getMessage()); } } @@ -653,11 +691,8 @@ public function debugWebDriverLogs(TestInterface $test = null) * Turns an array of log entries into a human-readable string. * Each log entry is an array with the keys "timestamp", "level", and "message". * See https://code.google.com/p/selenium/wiki/JsonWireProtocol#Log_Entry_JSON_Object - * - * @param array $logEntries - * @return string */ - protected function formatLogEntries(array $logEntries) + protected function formatLogEntries(array $logEntries): string { $formattedLogs = ''; @@ -668,20 +703,18 @@ protected function formatLogEntries(array $logEntries) '.' . ($logEntry['timestamp'] % 1000); $formattedLogs .= "{$time} {$logEntry['level']} - {$logEntry['message']}\n"; } + return $formattedLogs; } /** * Logs JavaScript errors as comments. - * - * @param ScenarioDriven $test - * @param array $browserLogEntries */ - protected function logJSErrors(ScenarioDriven $test, array $browserLogEntries) + protected function logJSErrors(ScenarioDriven $test, array $browserLogEntries): void { foreach ($browserLogEntries as $logEntry) { - if (true === isset($logEntry['level']) - && true === isset($logEntry['message']) + if (isset($logEntry['level']) + && isset($logEntry['message']) && $this->isJSError($logEntry['level'], $logEntry['message']) ) { // Timestamp is in milliseconds, but date() requires seconds. @@ -696,12 +729,8 @@ protected function logJSErrors(ScenarioDriven $test, array $browserLogEntries) /** * Determines if the log entry is an error. * The decision is made depending on browser and log-level. - * - * @param string $logEntryLevel - * @param string $message - * @return bool */ - protected function isJSError($logEntryLevel, $message) + protected function isJSError(string $logEntryLevel, string $message): bool { return ( @@ -717,11 +746,12 @@ public function _afterSuite() $this->stopAllSessions(); } - protected function stopAllSessions() + protected function stopAllSessions(): void { foreach ($this->sessions as $session) { $this->_closeSession($session); } + $this->webDriver = null; $this->baseElement = null; } @@ -729,8 +759,8 @@ protected function stopAllSessions() public function amOnSubdomain($subdomain) { $url = $this->config['url']; - $url = preg_replace('~(https?:\/\/)(.*\.)(.*\.)~', "$1$3", $url); // removing current subdomain - $url = preg_replace('~(https?:\/\/)(.*)~', "$1$subdomain.$2", $url); // inserting new + $url = preg_replace('#(https?:\/\/)(.*\.)(.*\.)#', "$1$3", $url); // removing current subdomain + $url = preg_replace('#(https?:\/\/)(.*)#', sprintf('$1%s.$2', $subdomain), $url); // inserting new $this->_reconfigure(['url' => $url]); } @@ -749,10 +779,11 @@ public function _getUrl() "Module connection failure. The URL for client can't bre retrieved" ); } + return $this->config['url']; } - protected function getProxy() + protected function getProxy(): ?array { $proxyConfig = []; if ($this->config['http_proxy']) { @@ -761,57 +792,65 @@ protected function getProxy() $proxyConfig['httpProxy'] .= ':' . $this->config['http_proxy_port']; } } + if ($this->config['ssl_proxy']) { $proxyConfig['sslProxy'] = $this->config['ssl_proxy']; if ($this->config['ssl_proxy_port']) { $proxyConfig['sslProxy'] .= ':' . $this->config['ssl_proxy_port']; } } + if (!empty($proxyConfig)) { $proxyConfig['proxyType'] = 'manual'; return $proxyConfig; } + return null; } /** * Uri of currently opened page. - * @return string * @api * @throws ModuleException */ - public function _getCurrentUri() + public function _getCurrentUri(): string { $url = $this->webDriver->getCurrentURL(); if ($url == 'about:blank' || strpos($url, 'data:') === 0) { throw new ModuleException($this, 'Current url is blank, no page was opened'); } + return Uri::retrieveUri($url); } - public function _saveScreenshot($filename) + public function _saveScreenshot(string $filename) { - if (!isset($this->webDriver)) { + if ($this->webDriver === null) { $this->debug('WebDriver::_saveScreenshot method has been called when webDriver is not set'); return; } + try { $this->webDriver->takeScreenshot($filename); - } catch (\Exception $e) { + } catch (Exception $e) { $this->debug('Unable to retrieve screenshot from Selenium : ' . $e->getMessage()); return; } } - public function _saveElementScreenshot($selector, $filename) + /** + * @param WebDriverBy|array $selector + */ + public function _saveElementScreenshot($selector, string $filename): void { - if (!isset($this->webDriver)) { + if ($this->webDriver === null) { $this->debug('WebDriver::_saveElementScreenshot method has been called when webDriver is not set'); return; } + try { $this->matchFirstOrFail($this->webDriver, $selector)->takeElementScreenshot($filename); - } catch (\Exception $e) { + } catch (Exception $e) { $this->debug('Unable to retrieve element screenshot from Selenium : ' . $e->getMessage()); return; } @@ -824,17 +863,17 @@ public function _findElements($locator) /** * Saves HTML source of a page to a file - * @param $filename */ public function _savePageSource($filename) { - if (!isset($this->webDriver)) { + if ($this->webDriver === null) { $this->debug('WebDriver::_savePageSource method has been called when webDriver is not set'); return; } + try { file_put_contents($filename, $this->webDriver->getPageSource()); - } catch (\Exception $e) { + } catch (Exception $e) { $this->debug('Unable to retrieve source page from Selenium : ' . $e->getMessage()); } } @@ -850,21 +889,21 @@ public function _savePageSource($filename) * $I->makeScreenshot(); * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.png * ``` - * - * @param $name */ - public function makeScreenshot($name = null) + public function makeScreenshot(string $name = null): void { if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); } + $debugDir = codecept_log_dir() . 'debug'; if (!is_dir($debugDir)) { - mkdir($debugDir, 0777); + mkdir($debugDir); } + $screenName = $debugDir . DIRECTORY_SEPARATOR . $name . '.png'; $this->_saveScreenshot($screenName); - $this->debugSection('Screenshot Saved', "file://$screenName"); + $this->debugSection('Screenshot Saved', "file://{$screenName}"); } /** @@ -879,20 +918,22 @@ public function makeScreenshot($name = null) * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.png * ``` * - * @param $name + * @param WebDriverBy|array $selector */ - public function makeElementScreenshot($selector, $name = null) + public function makeElementScreenshot($selector, string $name = null): void { if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); } + $debugDir = codecept_log_dir() . 'debug'; if (!is_dir($debugDir)) { - mkdir($debugDir, 0777); + mkdir($debugDir); } + $screenName = $debugDir . DIRECTORY_SEPARATOR . $name . '.png'; $this->_saveElementScreenshot($selector, $screenName); - $this->debugSection('Screenshot Saved', "file://$screenName"); + $this->debugSection('Screenshot Saved', "file://{$screenName}"); } public function makeHtmlSnapshot($name = null) @@ -900,14 +941,16 @@ public function makeHtmlSnapshot($name = null) if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); } + $debugDir = codecept_output_dir() . 'debug'; if (!is_dir($debugDir)) { - mkdir($debugDir, 0777); + mkdir($debugDir); } + $fileName = $debugDir . DIRECTORY_SEPARATOR . $name . '.html'; $this->_savePageSource($fileName); - $this->debugSection('Snapshot Saved', "file://$fileName"); + $this->debugSection('Snapshot Saved', "file://{$fileName}"); } @@ -920,32 +963,28 @@ public function makeHtmlSnapshot($name = null) * $I->resizeWindow(800, 600); * * ``` - * - * @param int $width - * @param int $height */ - public function resizeWindow($width, $height) + public function resizeWindow(int $width, int $height): void { $this->webDriver->manage()->window()->setSize(new WebDriverDimension($width, $height)); } - private function debugCookies() + private function debugCookies(): void { $result = []; $cookies = $this->webDriver->manage()->getCookies(); foreach ($cookies as $cookie) { $result[] = is_array($cookie) ? $cookie : $cookie->toArray(); } - $this->debugSection('Cookies', json_encode($result)); + + $this->debugSection('Cookies', json_encode($result, JSON_THROW_ON_ERROR)); } public function seeCookie($cookie, array $params = []) { $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params); $cookies = array_map( - function ($c) { - return $c['name']; - }, + fn($c) => $c['name'], $cookies ); $this->debugCookies(); @@ -956,9 +995,7 @@ public function dontSeeCookie($cookie, array $params = []) { $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params); $cookies = array_map( - function ($c) { - return $c['name']; - }, + fn($c) => $c['name'], $cookies ); $this->debugCookies(); @@ -985,6 +1022,7 @@ public function setCookie($cookie, $value, array $params = [], $showDebug = true $params[$key] = $default; } } + $this->webDriver->manage()->addCookie($params); if ($showDebug) { $this->debugCookies(); @@ -1004,6 +1042,7 @@ public function grabCookie($cookie, array $params = []) if (empty($cookies)) { return null; } + $cookie = reset($cookies); return $cookie['value']; } @@ -1012,10 +1051,9 @@ public function grabCookie($cookie, array $params = []) * Grabs current page source code. * * @throws ModuleException if no page was opened. - * * @return string Current page source code. */ - public function grabPageSource() + public function grabPageSource(): string { // Make sure that some page was opened. $this->_getCurrentUri(); @@ -1029,13 +1067,13 @@ protected function filterCookies($cookies, $params = []) if (!isset($params[$filter])) { continue; } + $cookies = array_filter( $cookies, - function ($item) use ($filter, $params) { - return $item[$filter] == $params[$filter]; - } + fn($item): bool => $item[$filter] == $params[$filter] ); } + return $cookies; } @@ -1059,6 +1097,7 @@ public function see($text, $selector = null) if (!$selector) { return $this->assertPageContains($text); } + $this->enableImplicitWait(); $nodes = $this->matchVisible($selector); $this->disableImplicitWait(); @@ -1070,6 +1109,7 @@ public function dontSee($text, $selector = null) if (!$selector) { return $this->assertPageNotContains($text); } + $nodes = $this->matchVisible($selector); $this->assertNodesNotContain($text, $nodes, $selector); } @@ -1091,29 +1131,23 @@ public function dontSeeInSource($raw) * seeInPageSource('assertThat( $this->webDriver->getPageSource(), - new PageConstraint($text, $this->_getCurrentUri()), - '' + new PageConstraint($text, $this->_getCurrentUri()) ); } /** * Checks that the page source doesn't contain the given string. - * - * @param $text */ - public function dontSeeInPageSource($text) + public function dontSeeInPageSource(string $text): void { $this->assertThatItsNot( $this->webDriver->getPageSource(), - new PageConstraint($text, $this->_getCurrentUri()), - '' + new PageConstraint($text, $this->_getCurrentUri()) ); } @@ -1123,18 +1157,22 @@ public function click($link, $context = null) if ($context) { $page = $this->matchFirstOrFail($this->webDriver, $context); } + $el = $this->_findClickable($page, $link); - if (!$el) { // check one more time if this was a CSS selector we didn't match + if ($el === null) { // check one more time if this was a CSS selector we didn't match try { $els = $this->match($page, $link); - } catch (MalformedLocatorException $e) { - throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button"); + } catch (MalformedLocatorException $exception) { + throw new ElementNotFound("name={$link}", "'{$link}' is invalid CSS and XPath selector and Link or Button"); } + $el = reset($els); } + if (!$el) { throw new ElementNotFound($link, 'Link or Button or CSS or XPath'); } + $el->click(); } @@ -1156,14 +1194,14 @@ public function click($link, $context = null) * $el = $module->_findClickable($topBar, 'Click Me'); * * ``` + * @param RemoteWebDriver|RemoteWebElement $page WebDriver instance or an element to search within + * @param WebDriverBy|array $link A link text or locator to click + * @return RemoteWebElement|WebDriverElement|null * @api - * @param RemoteWebDriver $page WebDriver instance or an element to search within - * @param $link a link text or locator to click - * @return WebDriverElement */ public function _findClickable($page, $link) { - if (is_array($link) or ($link instanceof WebDriverBy)) { + if (is_array($link) || $link instanceof WebDriverBy) { return $this->matchFirstOrFail($page, $link); } @@ -1172,33 +1210,33 @@ public function _findClickable($page, $link) return $this->matchFirstOrFail($page, $link); } - $locator = static::xpathLiteral(trim($link)); + $locator = static::xPathLiteral(trim((string) $link)); // narrow $xpath = Locator::combine( - ".//a[normalize-space(.)=$locator]", - ".//button[normalize-space(.)=$locator]", - ".//a/img[normalize-space(@alt)=$locator]/ancestor::a", - ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][normalize-space(@value)=$locator]" + ".//a[normalize-space(.)={$locator}]", + ".//button[normalize-space(.)={$locator}]", + ".//a/img[normalize-space(@alt)={$locator}]/ancestor::a", + ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][normalize-space(@value)={$locator}]" ); $els = $page->findElements(WebDriverBy::xpath($xpath)); - if (count($els)) { + if (count($els) > 0) { return reset($els); } // wide $xpath = Locator::combine( - ".//a[./@href][((contains(normalize-space(string(.)), $locator)) or contains(./@title, $locator) or .//img[contains(./@alt, $locator)])]", - ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][contains(./@value, $locator)]", - ".//input[./@type = 'image'][contains(./@alt, $locator)]", - ".//button[contains(normalize-space(string(.)), $locator)]", - ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][./@name = $locator or ./@title = $locator]", - ".//button[./@name = $locator or ./@title = $locator]" + ".//a[./@href][((contains(normalize-space(string(.)), {$locator})) or contains(./@title, {$locator}) or .//img[contains(./@alt, {$locator})])]", + ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][contains(./@value, {$locator})]", + ".//input[./@type = 'image'][contains(./@alt, {$locator})]", + ".//button[contains(normalize-space(string(.)), {$locator})]", + ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][./@name = {$locator} or ./@title = {$locator}]", + ".//button[./@name = {$locator} or ./@title = {$locator}]" ); $els = $page->findElements(WebDriverBy::xpath($xpath)); - if (count($els)) { + if (count($els) > 0) { return reset($els); } @@ -1206,30 +1244,32 @@ public function _findClickable($page, $link) } /** - * @param $selector - * @return WebDriverElement[] - * @throws \Codeception\Exception\ElementNotFound + * @param WebDriverElement|WebDriverBy|array|string $selector + * @return RemoteWebElement[]|WebDriverElement[] + * @throws ElementNotFound */ - protected function findFields($selector) + protected function findFields($selector): array { if ($selector instanceof WebDriverElement) { return [$selector]; } + if (is_array($selector) || ($selector instanceof WebDriverBy)) { $fields = $this->match($this->webDriver, $selector); if (empty($fields)) { throw new ElementNotFound($selector); } + return $fields; } - $locator = static::xpathLiteral(trim($selector)); + $locator = static::xPathLiteral(trim((string) $selector)); // by text or label $xpath = Locator::combine( // @codingStandardsIgnoreStart - ".//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = $locator) or ./@id = //label[contains(normalize-space(string(.)), $locator)]/@for) or ./@placeholder = $locator)]", - ".//label[contains(normalize-space(string(.)), $locator)]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" + ".//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = {$locator}) or ./@id = //label[contains(normalize-space(string(.)), {$locator})]/@for) or ./@placeholder = {$locator})]", + ".//label[contains(normalize-space(string(.)), {$locator})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" // @codingStandardsIgnoreEnd ); $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath)); @@ -1238,7 +1278,7 @@ protected function findFields($selector) } // by name - $xpath = ".//*[self::input | self::textarea | self::select][@name = $locator]"; + $xpath = ".//*[self::input | self::textarea | self::select][@name = {$locator}]"; $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath)); if (!empty($fields)) { return $fields; @@ -1254,9 +1294,8 @@ protected function findFields($selector) } /** - * @param $selector - * @return WebDriverElement - * @throws \Codeception\Exception\ElementNotFound + * @return WebDriverElement|false + * @throws ElementNotFound */ protected function findField($selector) { @@ -1272,12 +1311,14 @@ public function seeLink($text, $url = null) $currentUri = $this->_getCurrentUri(); if (empty($nodes)) { - $this->fail("No links containing text '$text' were found in page $currentUri"); + $this->fail("No links containing text '{$text}' were found in page {$currentUri}"); } + if ($url) { $nodes = $this->filterNodesByHref($url, $nodes); } - $this->assertNotEmpty($nodes, "No links containing text '$text' and URL '$url' were found in page $currentUri"); + + $this->assertNotEmpty($nodes, "No links containing text '{$text}' and URL '{$url}' were found in page {$currentUri}"); } public function dontSeeLink($text, $url = null) @@ -1285,32 +1326,25 @@ public function dontSeeLink($text, $url = null) $nodes = $this->getBaseElement()->findElements(WebDriverBy::partialLinkText($text)); $currentUri = $this->_getCurrentUri(); if (!$url) { - $this->assertEmpty($nodes, "Link containing text '$text' was found in page $currentUri"); + $this->assertEmpty($nodes, "Link containing text '{$text}' was found in page {$currentUri}"); } else { $nodes = $this->filterNodesByHref($url, $nodes); - $this->assertEmpty($nodes, "Link containing text '$text' and URL '$url' was found in page $currentUri"); + $this->assertEmpty($nodes, "Link containing text '{$text}' and URL '{$url}' was found in page {$currentUri}"); } } - /** - * @param string $url - * @param $nodes - * @return array - */ - private function filterNodesByHref($url, $nodes) + private function filterNodesByHref(string $url, array $nodes): ?array { //current uri can be relative, merging it with configured base url gives absolute url $absoluteCurrentUrl = Uri::mergeUrls($this->_getUrl(), $this->_getCurrentUri()); $expectedUrl = Uri::mergeUrls($absoluteCurrentUrl, $url); - - $nodes = array_filter( + return array_filter( $nodes, - function (WebDriverElement $e) use ($expectedUrl, $absoluteCurrentUrl) { + function (WebDriverElement $e) use ($expectedUrl, $absoluteCurrentUrl): bool { $elementHref = Uri::mergeUrls($absoluteCurrentUrl, $e->getAttribute('href')); return $elementHref === $expectedUrl; } ); - return $nodes; } public function seeInCurrentUrl($uri) @@ -1348,14 +1382,17 @@ public function grabFromCurrentUrl($uri = null) if (!$uri) { return $this->_getCurrentUri(); } + $matches = []; $res = preg_match($uri, $this->_getCurrentUri(), $matches); if (!$res) { - $this->fail("Couldn't match $uri in " . $this->_getCurrentUri()); + $this->fail("Couldn't match {$uri} in " . $this->_getCurrentUri()); } + if (!isset($matches[1])) { $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'"); } + return $matches[1]; } @@ -1397,6 +1434,7 @@ protected function proceedSeeInFormFields($formSelector, array $params, $assertN if (empty($form)) { throw new ElementNotFound($formSelector, "Form via CSS or XPath"); } + $form = reset($form); $els = []; @@ -1405,7 +1443,7 @@ protected function proceedSeeInFormFields($formSelector, array $params, $assertN } foreach ($els as $arrayElement) { - list($el, $values) = $arrayElement; + [$el, $values] = $arrayElement; if (!is_array($values)) { $values = [$values]; @@ -1430,17 +1468,16 @@ protected function proceedSeeInFormFields($formSelector, array $params, $assertN * @param RemoteWebElement $form The form in which to search for fields. * @param string $name The field's name. * @param mixed $values - * @return void */ - protected function pushFormField(&$els, $form, $name, $values) + protected function pushFormField(array &$els, RemoteWebElement $form, string $name, $values): void { $el = $form->findElements(WebDriverBy::name($name)); - if ($el) { + if ($el !== []) { $els[] = [$el, $values]; } elseif (is_array($values)) { foreach ($values as $key => $value) { - $this->pushFormField($els, $form, "{$name}[$key]", $value); + $this->pushFormField($els, $form, "{$name}[{$key}]", $value); } } else { throw new ElementNotFound($name); @@ -1449,10 +1486,9 @@ protected function pushFormField(&$els, $form, $name, $values) /** * @param RemoteWebElement[] $elements - * @param $value - * @return array + * @param mixed $value */ - protected function proceedSeeInField(array $elements, $value) + protected function proceedSeeInField(array $elements, $value): array { $strField = reset($elements)->getAttribute('name'); if (reset($elements)->getTagName() === 'select') { @@ -1467,6 +1503,7 @@ protected function proceedSeeInField(array $elements, $value) if (is_bool($value)) { $currentValues = [false]; } + foreach ($elements as $el) { switch ($el->getTagName()) { case 'input': @@ -1482,12 +1519,14 @@ protected function proceedSeeInField(array $elements, $value) } else { $currentValues[] = $el->getAttribute('value'); } + break; case 'option': // no break we need the trim text and the value also if (!$el->isSelected()) { break; } + $currentValues[] = $el->getText(); case 'textarea': // we include trimmed and real value of textarea for check @@ -1502,7 +1541,7 @@ protected function proceedSeeInField(array $elements, $value) 'Contains', $value, $currentValues, - "Failed testing for '$value' in $strField's value: '" . implode("', '", $currentValues) . "'" + "Failed testing for '{$value}' in {$strField}'s value: '" . implode("', '", $currentValues) . "'" ]; } @@ -1518,9 +1557,11 @@ public function selectOption($select, $option) break; } } + if (!$radio) { - throw new ElementNotFound($select, "Radiobutton with value or name '$option in"); + throw new ElementNotFound($select, "Radiobutton with value or name '{$option} in"); } + $radio->click(); return; } @@ -1529,6 +1570,7 @@ public function selectOption($select, $option) if ($wdSelect->isMultiple()) { $wdSelect->deselectAll(); } + if (!is_array($option)) { $option = [$option]; } @@ -1540,7 +1582,7 @@ public function selectOption($select, $option) try { $wdSelect->selectByVisibleText($opt); $matched = true; - } catch (NoSuchElementException $e) { + } catch (NoSuchElementException $exception) { } } } @@ -1554,7 +1596,7 @@ public function selectOption($select, $option) try { $wdSelect->selectByValue($opt); $matched = true; - } catch (NoSuchElementException $e) { + } catch (NoSuchElementException $exception) { } } } @@ -1571,14 +1613,16 @@ public function selectOption($select, $option) if (!$optElement->isSelected()) { $optElement->click(); } - } catch (NoSuchElementException $e) { + } catch (NoSuchElementException $exception) { // exception treated at the end } } + if ($matched) { return; } - throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value"); + + throw new ElementNotFound(json_encode($option, JSON_THROW_ON_ERROR), "Option inside {$select} matched by name or value"); } /** @@ -1606,10 +1650,11 @@ public function _initializeSession() if (!is_null($this->config['pageload_timeout'])) { $this->webDriver->manage()->timeouts()->pageLoadTimeout($this->config['pageload_timeout']); } + $this->setBaseElement(); $this->initialWindowSize(); - } catch (WebDriverCurlException $e) { - codecept_debug('Curl error: ' . $e->getMessage()); + } catch (WebDriverCurlException $exception) { + codecept_debug('Curl error: ' . $exception->getMessage()); throw new ConnectionException("Can't connect to WebDriver at {$this->wdHost}. Make sure that ChromeDriver, GeckoDriver or Selenium Server is running."); } } @@ -1617,8 +1662,8 @@ public function _initializeSession() /** * Loads current RemoteWebDriver instance as a session * - * @api * @param RemoteWebDriver $session + * @api */ public function _loadSession($session) { @@ -1630,7 +1675,6 @@ public function _loadSession($session) * Returns current WebDriver session for saving * * @api - * @return RemoteWebDriver */ public function _backupSession() { @@ -1654,18 +1698,18 @@ public function _backupSession() */ public function _closeSession($webDriver = null) { - if (!$webDriver and $this->webDriver) { + if (!$webDriver && $this->webDriver) { $webDriver = $this->webDriver; } + if (!$webDriver) { return; } + try { $webDriver->quit(); unset($webDriver); - } catch (UnknownServerException $e) { - // Session already closed so nothing to do - } catch (UnknownErrorException $e) { + } catch (UnknownErrorException $exception) { // Session already closed so nothing to do } } @@ -1673,10 +1717,9 @@ public function _closeSession($webDriver = null) /** * Unselect an option in the given select box. * - * @param $select - * @param $option + * @param mixed $option */ - public function unselectOption($select, $option) + public function unselectOption(WebDriverElement $select, $option): void { $el = $this->findField($select); @@ -1707,75 +1750,81 @@ public function unselectOption($select, $option) if ($matched) { return; } - throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value"); + + throw new ElementNotFound(json_encode($option), "Option inside {$select} matched by name or value"); } /** * @param $context - * @param $radioOrCheckbox - * @param bool $byValue - * @return mixed|null + * @param WebDriverElement|WebDriverBy $radioOrCheckbox + * @return mixed */ - protected function findCheckable($context, $radioOrCheckbox, $byValue = false) + protected function findCheckable($context, $radioOrCheckbox, bool $byValue = false) { if ($radioOrCheckbox instanceof WebDriverElement) { return $radioOrCheckbox; } - if (is_array($radioOrCheckbox) or ($radioOrCheckbox instanceof WebDriverBy)) { + if (is_array($radioOrCheckbox) || $radioOrCheckbox instanceof WebDriverBy) { return $this->matchFirstOrFail($this->getBaseElement(), $radioOrCheckbox); } - $locator = static::xpathLiteral($radioOrCheckbox); + $locator = static::xPathLiteral($radioOrCheckbox); if ($context instanceof WebDriverElement && $context->getTagName() === 'input') { $contextType = $context->getAttribute('type'); if (!in_array($contextType, ['checkbox', 'radio'], true)) { return null; } - $nameLiteral = static::xpathLiteral($context->getAttribute('name')); - $typeLiteral = static::xpathLiteral($contextType); - $inputLocatorFragment = "input[@type = $typeLiteral][@name = $nameLiteral]"; + + $nameLiteral = static::xPathLiteral($context->getAttribute('name')); + $typeLiteral = static::xPathLiteral($contextType); + $inputLocatorFragment = "input[@type = {$typeLiteral}][@name = {$nameLiteral}]"; $xpath = Locator::combine( // @codingStandardsIgnoreStart - "ancestor::form//{$inputLocatorFragment}[(@id = ancestor::form//label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator]", + "ancestor::form//{$inputLocatorFragment}[(@id = ancestor::form//label[contains(normalize-space(string(.)), {$locator})]/@for) or @placeholder = {$locator}]", // @codingStandardsIgnoreEnd - "ancestor::form//label[contains(normalize-space(string(.)), $locator)]//{$inputLocatorFragment}" + "ancestor::form//label[contains(normalize-space(string(.)), {$locator})]//{$inputLocatorFragment}" ); if ($byValue) { - $xpath = Locator::combine($xpath, "ancestor::form//{$inputLocatorFragment}[@value = $locator]"); + $xpath = Locator::combine($xpath, "ancestor::form//{$inputLocatorFragment}[@value = {$locator}]"); } } else { $xpath = Locator::combine( // @codingStandardsIgnoreStart - "//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator or @name = $locator]", + "//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[contains(normalize-space(string(.)), {$locator})]/@for) or @placeholder = {$locator} or @name = {$locator}]", // @codingStandardsIgnoreEnd - "//label[contains(normalize-space(string(.)), $locator)]//input[@type = 'radio' or @type = 'checkbox']" + "//label[contains(normalize-space(string(.)), {$locator})]//input[@type = 'radio' or @type = 'checkbox']" ); if ($byValue) { - $xpath = Locator::combine($xpath, "//input[@type = 'checkbox' or @type = 'radio'][@value = $locator]"); + $xpath = Locator::combine($xpath, sprintf("//input[@type = 'checkbox' or @type = 'radio'][@value = %s]", $locator)); } } + $els = $context->findElements(WebDriverBy::xpath($xpath)); - if (count($els)) { + if (count($els) > 0) { return reset($els); } + $els = $context->findElements(WebDriverBy::xpath(str_replace('ancestor::form', '', $xpath))); - if (count($els)) { + if (count($els) > 0) { return reset($els); } + $els = $this->match($context, $radioOrCheckbox); - if (count($els)) { + if (count($els) > 0) { return reset($els); } + return null; } - protected function matchCheckables($selector) + protected function matchCheckables($selector): array { $els = $this->match($this->webDriver, $selector); - if (!count($els)) { + if ($els === []) { throw new ElementNotFound($selector, "Element containing radio by CSS or XPath"); } + return $els; } @@ -1785,9 +1834,11 @@ public function checkOption($option) if (!$field) { throw new ElementNotFound($option, "Checkbox or Radio by Label or CSS or XPath"); } + if ($field->isSelected()) { return; } + $field->click(); } @@ -1797,9 +1848,11 @@ public function uncheckOption($option) if (!$field) { throw new ElementNotFound($option, "Checkbox by Label or CSS or XPath"); } + if (!$field->isSelected()) { return; } + $field->click(); } @@ -1818,9 +1871,9 @@ public function fillField($field, $value) * $I->clearField('#username'); * ``` * - * @param $field + * @param mixed $field */ - public function clearField($field) + public function clearField($field): void { $el = $this->findField($field); $el->clear(); @@ -1828,32 +1881,33 @@ public function clearField($field) /** * Type in characters on active element. - * With a second parameter you can specify delay between key presses. - * + * With a second parameter you can specify delay between key presses. + * * ```php * click('#input'); - * + * * // type text in active element * $I->type('Hello world'); - * + * * // type text with a 1sec delay between chars * $I->type('Hello World', 1); * ``` - * + * * This might be useful when you an input reacts to typing and you need to slow it down to emulate human behavior. * For instance, this is how Credit Card fields can be filled in. - * - * @param $text - * @param $delay [sec] + * + * @param int $delay [sec] */ - public function type($text, $delay = 0) { + public function type(string $text, int $delay = 0): void + { $keys = str_split($text); foreach ($keys as $key) { sleep($delay); $this->webDriver->getKeyboard()->pressKey($key); } + sleep($delay); } @@ -1863,11 +1917,13 @@ public function attachFile($field, $filename) // in order to be compatible on different OS $filePath = codecept_data_dir() . $filename; if (!file_exists($filePath)) { - throw new \InvalidArgumentException("File does not exist: $filePath"); + throw new InvalidArgumentException("File does not exist: {$filePath}"); } + if (!is_readable($filePath)) { - throw new \InvalidArgumentException("File is not readable: $filePath"); + throw new InvalidArgumentException("File is not readable: {$filePath}"); } + // in order for remote upload to be enabled $el->setFileDetector(new LocalFileDetector()); @@ -1875,35 +1931,38 @@ public function attachFile($field, $filename) if ($this->isPhantom()) { $el->setFileDetector(new UselessFileDetector()); } + $el->sendKeys(realpath($filePath)); } /** * Grabs all visible text from the current page. - * - * @return string */ - protected function getVisibleText() + protected function getVisibleText(): ?string { if ($this->getBaseElement() instanceof RemoteWebElement) { return $this->getBaseElement()->getText(); } + $els = $this->getBaseElement()->findElements(WebDriverBy::cssSelector('body')); if (isset($els[0])) { return $els[0]->getText(); } + return ''; } public function grabTextFrom($cssOrXPathOrRegex) { $els = $this->match($this->getBaseElement(), $cssOrXPathOrRegex, false); - if (count($els)) { + if ($els !== []) { return $els[0]->getText(); } + if (@preg_match($cssOrXPathOrRegex, $this->webDriver->getPageSource(), $matches)) { return $matches[1]; } + throw new ElementNotFound($cssOrXPathOrRegex, 'CSS or XPath or Regex'); } @@ -1921,6 +1980,7 @@ public function grabValueFrom($field) $select = new WebDriverSelect($el); return $select->getFirstSelectedOption()->getAttribute('value'); } + return $el->getAttribute('value'); } @@ -1928,27 +1988,26 @@ public function grabMultiple($cssOrXpath, $attribute = null) { $els = $this->match($this->getBaseElement(), $cssOrXpath); return array_map( - function (WebDriverElement $e) use ($attribute) { + function (WebDriverElement $e) use ($attribute): ?string { if ($attribute) { return $e->getAttribute($attribute); } + return $e->getText(); }, $els ); } - protected function filterByAttributes($els, array $attributes) { foreach ($attributes as $attr => $value) { $els = array_filter( $els, - function (WebDriverElement $el) use ($attr, $value) { - return $el->getAttribute($attr) == $value; - } + fn(WebDriverElement $el): bool => $el->getAttribute($attr) == $value ); } + return $els; } @@ -1974,13 +2033,11 @@ public function dontSeeElement($selector, $attributes = []) * ``` php * seeElementInDOM('//form/input[type=hidden]'); - * ?> * ``` * * @param $selector - * @param array $attributes */ - public function seeElementInDOM($selector, $attributes = []) + public function seeElementInDOM($selector, array $attributes = []): void { $this->enableImplicitWait(); $els = $this->match($this->getBaseElement(), $selector); @@ -1993,10 +2050,9 @@ public function seeElementInDOM($selector, $attributes = []) /** * Opposite of `seeElementInDOM`. * - * @param $selector - * @param array $attributes + * @param mixed $selector */ - public function dontSeeElementInDOM($selector, $attributes = []) + public function dontSeeElementInDOM($selector, array $attributes = []): void { $els = $this->match($this->getBaseElement(), $selector); $els = $this->filterByAttributes($els, $attributes); @@ -2007,7 +2063,7 @@ public function seeNumberOfElements($selector, $expected) { $counted = count($this->matchVisible($selector)); if (is_array($expected)) { - list($floor, $ceil) = $expected; + [$floor, $ceil] = $expected; $this->assertTrue( $floor <= $counted && $ceil >= $counted, 'Number of elements counted differs from expected range' @@ -2025,7 +2081,7 @@ public function seeNumberOfElementsInDOM($selector, $expected) { $counted = count($this->match($this->getBaseElement(), $selector)); if (is_array($expected)) { - list($floor, $ceil) = $expected; + [$floor, $ceil] = $expected; $this->assertTrue( $floor <= $counted && $ceil >= $counted, 'Number of elements counted differs from expected range' @@ -2047,16 +2103,16 @@ public function seeOptionIsSelected($selector, $optionText) foreach ($els as $k => $el) { $els[$k] = $this->findCheckable($el, $optionText, true); } + $this->assertNotEmpty( array_filter( $els, - function ($e) { - return $e && $e->isSelected(); - } + fn($e): bool => $e && $e->isSelected() ) ); return; } + $select = new WebDriverSelect($el); $this->assertNodesContain($optionText, $select->getAllSelectedOptions(), 'option'); } @@ -2069,16 +2125,16 @@ public function dontSeeOptionIsSelected($selector, $optionText) foreach ($els as $k => $el) { $els[$k] = $this->findCheckable($el, $optionText, true); } + $this->assertEmpty( array_filter( $els, - function ($e) { - return $e && $e->isSelected(); - } + fn($e): bool => $e && $e->isSelected() ) ); return; } + $select = new WebDriverSelect($el); $this->assertNodesNotContain($optionText, $select->getAllSelectedOptions(), 'option'); } @@ -2098,22 +2154,24 @@ public function dontSeeInTitle($title) * Don't confuse popups with modal windows, * as created by [various libraries](http://jster.net/category/windows-modals-popups). */ - public function acceptPopup() + public function acceptPopup(): void { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } + $this->webDriver->switchTo()->alert()->accept(); } /** * Dismisses the active JavaScript popup, as created by `window.alert`, `window.confirm`, or `window.prompt`. */ - public function cancelPopup() + public function cancelPopup(): void { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } + $this->webDriver->switchTo()->alert()->dismiss(); } @@ -2123,19 +2181,20 @@ public function cancelPopup() * * @param $text * - * @throws \Codeception\Exception\ModuleException + * @throws ModuleException */ - public function seeInPopup($text) + public function seeInPopup($text): void { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } + $alert = $this->webDriver->switchTo()->alert(); try { $this->assertStringContainsString($text, $alert->getText()); - } catch (\PHPUnit\Framework\AssertionFailedError $e) { + } catch (PHPUnitAssertionFailedError $failedError) { $alert->dismiss(); - throw $e; + throw $failedError; } } @@ -2143,19 +2202,18 @@ public function seeInPopup($text) * Checks that the active JavaScript popup, * as created by `window.alert`|`window.confirm`|`window.prompt`, does NOT contain the given string. * - * @param $text - * - * @throws \Codeception\Exception\ModuleException + * @throws ModuleException */ - public function dontSeeInPopup($text) + public function dontSeeInPopup(string $text): void { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } + $alert = $this->webDriver->switchTo()->alert(); try { $this->assertStringNotContainsString($text, $alert->getText()); - } catch (\PHPUnit\Framework\AssertionFailedError $e) { + } catch (PHPUnitAssertionFailedError $e) { $alert->dismiss(); throw $e; } @@ -2165,21 +2223,21 @@ public function dontSeeInPopup($text) * Enters text into a native JavaScript prompt popup, as created by `window.prompt`. * * @param $keys - * - * @throws \Codeception\Exception\ModuleException + * @throws ModuleException */ - public function typeInPopup($keys) + public function typeInPopup($keys): void { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } + $this->webDriver->switchTo()->alert()->sendKeys($keys); } /** * Reloads the current page. */ - public function reloadPage() + public function reloadPage(): void { $this->webDriver->navigate()->refresh(); } @@ -2187,7 +2245,7 @@ public function reloadPage() /** * Moves back in history. */ - public function moveBack() + public function moveBack(): void { $this->webDriver->navigate()->back(); $this->debug($this->_getCurrentUri()); @@ -2196,17 +2254,18 @@ public function moveBack() /** * Moves forward in history. */ - public function moveForward() + public function moveForward(): void { $this->webDriver->navigate()->forward(); $this->debug($this->_getCurrentUri()); } - protected function getSubmissionFormFieldName($name) + protected function getSubmissionFormFieldName(string $name): string { if (substr($name, -2) === '[]') { return substr($name, 0, -2); } + return $name; } @@ -2293,7 +2352,6 @@ protected function getSubmissionFormFieldName($name) * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); * // $I->amOnPage('/path/to/form-page') may be needed * $I->seeInFormFields('//form[@id=my-form]', $form); - * ?> * ``` * * Parameter values must be set to arrays for multiple input fields @@ -2319,7 +2377,6 @@ protected function getSubmissionFormFieldName($name) * 'second option value', * ] * ]); - * ?> * ``` * * Mixing string and boolean values for a checkbox's value is not supported @@ -2364,10 +2421,6 @@ protected function getSubmissionFormFieldName($name) * - 'submitButton' * - ['name' => 'submitButton'] * - WebDriverBy::name('submitButton') - * - * @param $selector - * @param $params - * @param $button */ public function submitForm($selector, array $params, $button = null) { @@ -2381,6 +2434,7 @@ public function submitForm($selector, array $params, $button = null) if (!isset($params[$fieldName])) { continue; } + $value = $params[$fieldName]; if (is_array($value) && $field->getTagName() !== 'select') { if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') { @@ -2393,6 +2447,7 @@ public function submitForm($selector, array $params, $button = null) break; } } + if (!$found && !empty($value) && is_bool(reset($value))) { $value = array_pop($params[$fieldName]); } @@ -2421,7 +2476,7 @@ public function submitForm($selector, array $params, $button = null) $form->getAttribute('action') ? $form->getAttribute('action') : $this->_getCurrentUri() ); $this->debugSection('Method', $form->getAttribute('method') ? $form->getAttribute('method') : 'GET'); - $this->debugSection('Parameters', json_encode($params)); + $this->debugSection('Parameters', json_encode($params, JSON_THROW_ON_ERROR)); $submitted = false; if (!empty($button)) { @@ -2445,6 +2500,7 @@ public function submitForm($selector, array $params, $button = null) if (!$submitted) { $form->submit(); } + $this->debugSection('Page', $this->_getCurrentUri()); } @@ -2459,20 +2515,16 @@ public function submitForm($selector, array $params, $button = null) * $I->waitForElementChange('#menu', function(WebDriverElement $el) { * return $el->isDisplayed(); * }, 100); - * ?> * ``` * * @param $element - * @param \Closure $callback * @param int $timeout seconds - * @throws \Codeception\Exception\ElementNotFound + * @throws ElementNotFound */ - public function waitForElementChange($element, \Closure $callback, $timeout = 30) + public function waitForElementChange($element, Closure $callback, int $timeout = 30): void { $el = $this->matchFirstOrFail($this->getBaseElement(), $element); - $checker = function () use ($el, $callback) { - return $callback($el); - }; + $checker = fn() => $callback($el); $this->webDriver->wait($timeout)->until($checker); } @@ -2484,14 +2536,13 @@ public function waitForElementChange($element, \Closure $callback, $timeout = 30 * waitForElement('#agree_button', 30); // secs * $I->click('#agree_button'); - * ?> * ``` * * @param $element * @param int $timeout seconds - * @throws \Exception + * @throws Exception */ - public function waitForElement($element, $timeout = 10) + public function waitForElement($element, int $timeout = 10): void { $condition = WebDriverExpectedCondition::presenceOfElementLocated($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); @@ -2505,14 +2556,13 @@ public function waitForElement($element, $timeout = 10) * waitForElementVisible('#agree_button', 30); // secs * $I->click('#agree_button'); - * ?> * ``` * * @param $element * @param int $timeout seconds - * @throws \Exception + * @throws Exception */ - public function waitForElementVisible($element, $timeout = 10) + public function waitForElementVisible($element, int $timeout = 10): void { $condition = WebDriverExpectedCondition::visibilityOfElementLocated($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); @@ -2525,14 +2575,13 @@ public function waitForElementVisible($element, $timeout = 10) * ``` php * waitForElementNotVisible('#agree_button', 30); // secs - * ?> * ``` * * @param $element * @param int $timeout seconds - * @throws \Exception + * @throws Exception */ - public function waitForElementNotVisible($element, $timeout = 10) + public function waitForElementNotVisible($element, int $timeout = 10): void { $condition = WebDriverExpectedCondition::invisibilityOfElementLocated($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); @@ -2546,14 +2595,13 @@ public function waitForElementNotVisible($element, $timeout = 10) * waitForElementClickable('#agree_button', 30); // secs * $I->click('#agree_button'); - * ?> * ``` * * @param $element * @param int $timeout seconds - * @throws \Exception + * @throws Exception */ - public function waitForElementClickable($element, $timeout = 10) + public function waitForElementClickable($element, int $timeout = 10): void { $condition = WebDriverExpectedCondition::elementToBeClickable($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); @@ -2570,15 +2618,12 @@ public function waitForElementClickable($element, $timeout = 10) * waitForText('foo', 30); // secs * $I->waitForText('foo', 30, '.title'); // secs - * ?> * ``` * - * @param string $text * @param int $timeout seconds - * @param string $selector optional - * @throws \Exception + * @throws Exception */ - public function waitForText($text, $timeout = 10, $selector = null) + public function waitForText(string $text, int $timeout = 10, string $selector = null): void { $message = sprintf( 'Waited for %d secs but text %s still not found', @@ -2586,12 +2631,12 @@ public function waitForText($text, $timeout = 10, $selector = null) Locator::humanReadableString($text) ); if (!$selector) { - $condition = WebDriverExpectedCondition::textToBePresentInElement(WebDriverBy::xpath('//body'), $text); + $condition = WebDriverExpectedCondition::elementTextContains(WebDriverBy::xpath('//body'), $text); $this->webDriver->wait($timeout)->until($condition, $message); return; } - $condition = WebDriverExpectedCondition::textToBePresentInElement($this->getLocator($selector), $text); + $condition = WebDriverExpectedCondition::elementTextContains($this->getLocator($selector), $text); $this->webDriver->wait($timeout)->until($condition, $message); } @@ -2599,9 +2644,9 @@ public function waitForText($text, $timeout = 10, $selector = null) * Wait for $timeout seconds. * * @param int|float $timeout secs - * @throws \Codeception\Exception\TestRuntimeException + * @throws TestRuntimeException */ - public function wait($timeout) + public function wait($timeout): void { if ($timeout >= 1000) { throw new TestRuntimeException( @@ -2610,7 +2655,8 @@ public function wait($timeout) Please note that wait method accepts number of seconds as parameter." ); } - usleep($timeout * 1000000); + + usleep($timeout * 1_000_000); } /** @@ -2619,7 +2665,7 @@ public function wait($timeout) * * ``` php * $I->executeInSelenium(function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) { - * $webdriver->get('http://google.com'); + * $webdriver->get('https://google.com'); * }); * ``` * @@ -2628,9 +2674,9 @@ public function wait($timeout) * Try not to use this command on a regular basis. * If Codeception lacks a feature you need, please implement it and submit a patch. * - * @param \Closure $function + * @param Closure $function */ - public function executeInSelenium(\Closure $function) + public function executeInSelenium(Closure $function) { return $function($this->webDriver); } @@ -2642,7 +2688,7 @@ public function executeInSelenium(\Closure $function) * * Example: * ``` html - * + * * ``` * * ``` php @@ -2652,7 +2698,6 @@ public function executeInSelenium(\Closure $function) * $I->switchToWindow("another_window"); * # switch to parent window * $I->switchToWindow(); - * ?> * ``` * * If the window has no name, match it by switching to next active tab using `switchToNextTab` method. @@ -2666,12 +2711,9 @@ public function executeInSelenium(\Closure $function) * $last_window = end($handles); * $webdriver->switchTo()->window($last_window); * }); - * ?> * ``` - * - * @param string|null $name */ - public function switchToWindow($name = null) + public function switchToWindow(string $name = null): void { $this->webDriver->switchTo()->window($name); } @@ -2681,7 +2723,7 @@ public function switchToWindow($name = null) * * Example: * ``` html - *