webDriver = RemoteWebDriver::create( $this->seleniumHost, $this->buildCapabilities(), $this->timeoutInSeconds * 1000, $this->timeoutInSeconds * 1000 ); } protected function tearDown(): void { // Screenshot Failed Test if (in_array($this->getStatus(), array(BaseTestRunner::STATUS_ERROR, BaseTestRunner::STATUS_FAILURE))) { $this->screenshotFailedTest(); } parent::tearDown(); // Quit WebDriver Instance $this->webDriver->quit(); } protected function logIntoAdmin($eUserLogin, $waitForDashboard = true): void { $loginData = $this->getLoginCredentialsByType($eUserLogin); $this->url($loginData['url']); $this->waitUntilDomReadyState(); // Check if we landed on dashboard - default page after login redirect if ($this->elementExists('dashboard')) { return; } $this->byId("user_login")->sendKeys($loginData['user']); $this->byId("user_password")->sendKeys($loginData['password']); $this->byCssSelector("button.btn")->click(); // Wait for form to load 2fa elements & continue with testing $this->waitFormToLoadBySelector('form#frm_2fa_input'); // Validate code $oGoogle2FA = new cGoogle2FA(); $next2FACode = $oGoogle2FA->getGoogleAuthCode($loginData['2fa']); foreach (str_split($next2FACode) as $i => $char) { $selector = "input.code2fa[tabindex='" . ++$i . "']"; $this->elementWait($selector, self::CSS_SELECTOR); $this->byCssSelector($selector)->sendKeys($char); } if ($waitForDashboard) { $this->elementWait("dashboard", self::ID); } } protected function logout(bool $useFormButton = true) { // Check a way exists to log out $logoutButton = '#company_menu > ul > li.comp_profile > a'; if ( $useFormButton && $this->elementExists($logoutButton, self::CSS_SELECTOR, true) && $this->byCssSelector($logoutButton)->isDisplayed() ) { $this->scrollElementIntoView($logoutButton, self::CSS_SELECTOR); $this->moveToElement($logoutButton, self::CSS_SELECTOR); $this->byCssSelector($logoutButton)->click(); $this->elementWait('#comp_menu .logout', self::CSS_SELECTOR); $this->moveToElement('#comp_menu .logout', self::CSS_SELECTOR); $this->byCssSelector("#comp_menu .logout")->click(); } else { $this->url("?logout=1"); } $this->elementWait('user_login'); } protected function changeYear(int $year): void { $elementSelector = '#company_menu > ul > li.year_select > a'; if ( !$this->elementExists($elementSelector, self::CSS_SELECTOR, true) || !$this->byCssSelector($elementSelector)->isDisplayed() ) { return; } $this->scrollIntoViewHoverClick($elementSelector, self::CSS_SELECTOR); $this->elementWait('#year_menu', self::CSS_SELECTOR); $this->scrollIntoViewHoverClick('#year_menu a[data-year="' . $year . '"]', self::CSS_SELECTOR); $this->waitUntilDomReadyState(); } protected function logoutAdmin(bool $useFormButton = true) { // Check a way exists to log out $logoutButton = '.menu-cnt .settbox li.sett.last > a'; if ($useFormButton && $this->elementExists($logoutButton, self::CSS_SELECTOR)) { $this->moveToElement($logoutButton, self::CSS_SELECTOR); $this->elementWait('div.settbox_cnt.sub-menu ul', self::CSS_SELECTOR); $elementXpath = '//div[contains(@class,"settbox_cnt")]/ul/li/a/div[@class="ico-logout"]/..'; $this->moveToElement($elementXpath, self::XPATH); $this->byXPath($elementXpath)->click(); } else { $this->url("?logout=1"); } // Wait for redirect to complete $this->elementWait('user_login'); } /** * @param string|null $pageUrl * @param bool $overrideBaseUrl * @return string|void */ public function url(string $pageUrl = null, bool $overrideBaseUrl = false) { $url = ($overrideBaseUrl) ? $pageUrl : $this->applicationBaseUrl . $pageUrl; $this->webDriver->get($url); $this->waitUntilDomReadyState(); } public function title(): string { return $this->webDriver->getTitle(); } /** * @param WebDriverElement|string|null $frameElement */ public function frame($frameElement = null): void { if (is_null($frameElement)) { $this->webDriver->switchTo()->defaultContent(); } else { $this->webDriver->switchTo()->frame($frameElement); } } public function acceptAlert(): void { $this->webDriver->switchTo()->alert()->accept(); } public function dismissAlert(): void { $this->webDriver->switchTo()->alert()->dismiss(); } public function elementExists(string $elementSelector, ?string $elementSelectorType = null, bool $now = false): bool { try { /** Prevent checking for non-existent elements taking x seconds instead of 0. */ if ($now) { $this->webDriver->manage()->timeouts()->implicitlyWait(0); } /** * We use getRemoteWebElements instead of getRemoteWebElement because getRemoteWebElements * returns an empty array instead of throwing an exception if not found. */ $elements = $this->getRemoteWebElements($elementSelector, $elementSelectorType ?? self::ID); $exists = count($elements) > 0; /** Set the implicit wait back to what it was. */ if ($now) { $this->webDriver->manage()->timeouts()->implicitlyWait($this->timeoutWait); } return $exists; } catch (Exception $e) { // Restore wait even if an unexpected driver error occurs if ($now) { $this->webDriver->manage()->timeouts()->implicitlyWait($this->timeoutWait); } return false; } } public function elementHasClass(string $class, string $selector, string $selectorType = self::ID): bool { try { $classes = $this->getRemoteWebElement($selector, $selectorType)->getAttribute('class'); return in_array($class, explode(' ', $classes)); } catch (NoSuchElementException $exception) { return false; } } public function elementWaitRemoved(string $selector, string $selectorType = self::ID): void { try { $element = $this->getWebDriverBy($selector, $selectorType); $this->webDriver->wait($this->timeoutWait, $this->timeoutWaitInterval)->until( WebDriverExpectedCondition::invisibilityOfElementLocated($element), ); } catch (NoSuchElementException $exception) { } } /** * @param string $selector * @param string|null $selectorType * @param bool|true $waitUntilDomReadyState * @param bool|true $presenceOfElementLocated * @param bool|true $visibilityOfElementLocated * @param bool|true $elementToBeClickable * @return bool|void */ public function elementWait( string $selector, string $selectorType = self::ID, bool $waitUntilDomReadyState = true, bool $presenceOfElementLocated = true, bool $visibilityOfElementLocated = true, bool $elementToBeClickable = true ) { try { $element = $this->getWebDriverBy($selector, $selectorType); if ($waitUntilDomReadyState) { $this->waitUntilDomReadyState(); } if ($presenceOfElementLocated) { $this->webDriver->wait($this->timeoutWait, $this->timeoutWaitInterval)->until( WebDriverExpectedCondition::presenceOfElementLocated($element), ); } if ($visibilityOfElementLocated) { $this->webDriver->wait($this->timeoutWait, $this->timeoutWaitInterval)->until( WebDriverExpectedCondition::visibilityOfElementLocated($element), ); } if ($elementToBeClickable) { $this->webDriver->wait($this->timeoutWait, $this->timeoutWaitInterval)->until( WebDriverExpectedCondition::elementToBeClickable($element), ); } if ($visibilityOfElementLocated) { $this->webDriver->wait($this->timeoutWait, $this->timeoutWaitInterval)->until( WebDriverExpectedCondition::visibilityOf($this->webDriver->findElement($element)), ); } return; } catch (NoSuchElementException $exception) { return false; } catch (TimeOutException $exception) { return false; } catch (Exception $exception) { return false; } } /** * @param string $selector * @param string|null $selectorType * @return RemoteWebElement[] */ protected function getRemoteWebElements(string $selector, string $selectorType = self::ID): array { return $this->webDriver->findElements($this->getWebDriverBy($selector, $selectorType)); } /** * @param string $selector * @param string|null $selectorType * @return RemoteWebElement */ protected function getRemoteWebElement(string $selector, string $selectorType = self::ID): RemoteWebElement { return $this->webDriver->findElement($this->getWebDriverBy($selector, $selectorType)); } private function getWebDriverBy(string $selector, string $selectorType = self::ID): WebDriverBy { switch ($selectorType) { case self::CLASS_NAME: $element = WebDriverBy::className($selector); break; case self::CSS_SELECTOR: $element = WebDriverBy::cssSelector($selector); break; case self::NAME: $element = WebDriverBy::name($selector); break; case self::LINK_TEXT: $element = WebDriverBy::linkText($selector); break; case self::PARTIAL_LINK_TEXT: $element = WebDriverBy::partialLinkText($selector); break; case self::TAG_NAME: $element = WebDriverBy::tagName($selector); break; case self::XPATH: $element = WebDriverBy::xpath($selector); break; case self::ID: default: $element = WebDriverBy::id($selector); break; } return $element; } public function pageTitleWait(string $pageTitle): void { $this->webDriver->wait($this->timeoutWait, $this->timeoutWaitInterval)->until( WebDriverExpectedCondition::titleContains($pageTitle) ); } public function newWindowWait(): void { $this->webDriver->wait($this->timeoutWait, $this->timeoutWaitInterval)->until( WebDriverExpectedCondition::numberOfWindowsToBe(2) ); } public function byClassName(string $elementClassName): RemoteWebElement { return $this->webDriver->findElement(WebDriverBy::className($elementClassName)); } public function byCssSelector(string $elementCssSelector): RemoteWebElement { return $this->webDriver->findElement(WebDriverBy::cssSelector($elementCssSelector)); } /** * @return RemoteWebElement[] a list of all WebDriverElements, or an empty array if nothing */ public function elementsByCssSelector(string $elementCssSelector): array { return $this->webDriver->findElements(WebDriverBy::cssSelector($elementCssSelector)); } public function byId(string $elementId): RemoteWebElement { return $this->webDriver->findElement(WebDriverBy::id($elementId)); } public function byName(string $elementName): RemoteWebElement { return $this->webDriver->findElement(WebDriverBy::name($elementName)); } public function byLinkText(string $elementLinkText): RemoteWebElement { return $this->webDriver->findElement(WebDriverBy::linkText($elementLinkText)); } public function byPartialLinkText(string $elementPartialLinkText): RemoteWebElement { return $this->webDriver->findElement(WebDriverBy::partialLinkText($elementPartialLinkText)); } public function byTagName(string $elementTagName): RemoteWebElement { return $this->webDriver->findElement(WebDriverBy::tagName($elementTagName)); } public function byXPath(string $elementXPath): RemoteWebElement { return $this->webDriver->findElement(WebDriverBy::xpath($elementXPath)); } public function select(RemoteWebElement $element): WebDriverSelect { return new WebDriverSelect($element); } private function screenshotFailedTest(): void { if ($this->screenshotOnFailedTest !== true) { return; } if (!is_dir($this->screenshotPath)) { mkdir($this->screenshotPath, 0777, true); } if (glob($this->screenshotPath . '/' . $this->getName() . "*.png")) { array_map('unlink', glob($this->screenshotPath . '/' . $this->getName() . "*.png")); } $filePath = $this->screenshotPath . '/' . $this->getName() . '_' . time() . '.png'; $this->webDriver->takeScreenshot($filePath); } private function buildCapabilities(): DesiredCapabilities { $capabilities = null; switch ($this->seleniumBrowser) { case 'firefox': $profile = new FirefoxProfile(); $capabilities = DesiredCapabilities::firefox(); $profile->setPreference('browser.download.folderList', 2); $profile->setPreference('browser.download.dir', $this->applicationDownloadFolder); $profile->setPreference('browser.download.panel.shown', false); $profile->setPreference('browser.download.manager.showWhenStarting', false); $profile->setPreference('pdfjs.disabled', true); $profile->setPreference( 'browser.helperApps.neverAsk.saveToDisk', 'application/pdf, text/csv, application/xlsx, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ); $capabilities->setCapability(FirefoxDriver::PROFILE, $profile); break; case 'chrome': $options = new ChromeOptions(); $capabilities = DesiredCapabilities::chrome(); $options->setExperimentalOption('prefs', array( 'download.directory_upgrade' => true, 'download.prompt_for_download' => false, 'download.default_directory' => $this->applicationDownloadFolder, 'safebrowsing.enabled' => false, 'safebrowsing.disable_download_protection' => true, 'profile.default_content_settings.popups' => 0, 'profile.default_content_settings.multiple-automatic-downloads' => 1, 'plugins.always_open_pdf_externally' => true, 'plugins.plugins_disabled' => ['Chrome PDF Viewer'], )); $capabilities->setCapability(ChromeOptions::CAPABILITY_W3C, $options); break; default: $this->fail('Unsupported browser type!'); } return $capabilities; } public function clearTempFolder(string $dir = __DIR__ . '/../temp'): void { foreach (glob($dir . '/*', GLOB_MARK) as $file) { if (is_dir($file)) { $this->clearTempFolder($file); } else { unlink($file); } } } protected function getFileFromTempFolder(string $dir = __DIR__ . '/../temp'): ?string { for ($i = 1; $i < 30; $i++) { foreach (glob($dir . '/*', GLOB_MARK) as $file) { if (is_file($file)) { return $file; } } sleep(1); } return null; } protected function waitFormToLoadBySelector(string $selector): void { $this->webDriver->wait($this->timeoutWait, $this->timeoutWaitInterval)->until( function ($driver) use ($selector) { return $driver->findElements(WebDriverBy::cssSelector($selector)); }, 'Error element' ); } protected function waitUntilDomReadyState(): void { $this->webDriver->wait($this->timeoutWait, $this->timeoutWaitInterval)->until(function ($driver) { $jsScript = "return " . "document.readyState === 'complete' && " . // Initial document ready state "window.documentReloadIsUnderWay === undefined && " . // Custom reload condition test "(" . "typeof(jQuery) == 'undefined' || " . // None jquery pages error prevention "(" . "(" . "typeof jQuery.active === undefined || " . "jQuery.active === 0" . // jQuery active states e.g. ajax requests " ) && " . "jQuery(':animated').length === 0" . // jQuery's animation end state ")" . ") ? true : false"; return $driver->executeScript($jsScript); }); } protected function executeJS(string $script) { return $this->webDriver->executeScript($script); } protected function executeJSBlur(): void { $this->executeJS('$(":focus").blur();'); $this->waitUntilDomReadyState(); } protected function markWebDocumentAsBusyUntilPageReloads(): void { $this->webDriver->executeScript('window.documentReloadIsUnderWay = true;'); } protected function normalizeUrl(string $url, bool $endSlash = false): string { if (str_ends_with($url, '/')) { if (!$endSlash) $url = substr($url, 0, strlen($url)-1); } else { if ($endSlash) $url .= '/'; } if (str_starts_with($url, '/')) $url = substr($url, 1); if (!$url) $url = 'index'; return $url; } protected function mprint(): void { // Get all arguments $vPassedArguments = func_get_args(); if(!!$vPassedArguments){ $sDebugOutput = PHP_EOL . 'START DEBUG OUTPUT:' . PHP_EOL; foreach($vPassedArguments as $vData) $sDebugOutput .= PHP_EOL . print_r($vData, true) . PHP_EOL; echo $sDebugOutput . PHP_EOL . 'END OF DEBUG!'; }else { echo 'NO ARGUMENTS PASSED!'; } $this->tearDown(); } protected function scrollIntoViewHoverClick( string $selector, string $selectorType = self::ID, bool $elementClick = true ): RemoteWebElement { $element = $this->scrollElementIntoView($selector, $selectorType, 'true'); $this->moveToElement($selector, $selectorType); if ($elementClick) { $element->click(); } return $element; } protected function scrollElementIntoView( string $selector, string $selectorType = self::ID, string $options = '{behavior:"instant", block:"center", inline:"end"}' ): RemoteWebElement { $element = $this->getVisibleFormFieldElement($selector, $selectorType); $this->webDriver->executeScript("arguments[0].scrollIntoView($options);", [$element]); return $element; } /** * When invalid HTML contains duplicates like: * * * pick the one actually visible. */ public function getVisibleFormFieldElement(string $selector, string $selectorType = self::ID): RemoteWebElement { if ($selectorType === self::ID) { $css = ".doc .form-data #$selector"; $selType = self::CSS_SELECTOR; } else { // TODO: Needs to be tested $css = $selector; $selType = $selectorType; } // Ensure at least one exists $this->elementWait($css, $selType, true, true, false, false); $elements = $this->getRemoteWebElements($css, $selType); if (count($elements) === 0) { $this->fail("Field '$selector' not found by selector: $css and selector type: $selType"); } foreach ($elements as $el) { try { if ($el->isDisplayed()) { return $el; } } catch (\Exception $e) { // ignore and continue } } if (count($elements) === 1) { return $elements[0]; } // If nothing is "displayed": $this->fail("Field '$selector' was found (" . count($elements) . " matches) but none are displayed."); } protected function getElementPosition($element, array $elements): int { foreach ($elements as $index => $item) { if ($item->getID() === $element->getID()) { return $index; } } return -1; } protected function getItemHeight($element): int { return (int) $element->getSize()->getHeight(); } protected function switchToPageLanguage(string $id = siteLanguages::LANG_LV): void { $selectedLangSelector = 'footer div.inner-lang-block'; if ($this->elementExists("$selectedLangSelector > a.$id", self::CSS_SELECTOR, true)) { return; } // Click on the lang menu container and click on the language $this->scrollIntoViewHoverClick($selectedLangSelector, self::CSS_SELECTOR); $this->scrollIntoViewHoverClick("footer .inner-lang-block #lang li > a#$id", self::CSS_SELECTOR); $this->assertTrue($this->elementExists("$selectedLangSelector > a.$id", self::CSS_SELECTOR)); } protected function gotoLocationByClickingVisibleUrl( string $menuItem, string $waitPageTitle, string $menuParentItem = null, string $waitForElementSelector = null ): void { if ($this->webDriver->getTitle() === $waitPageTitle) { return; } if ($menuParentItem) { $this->scrollIntoViewHoverClick($this->getMenuItemSelectXpathByTitle($menuParentItem), self::XPATH); $this->waitUntilDomReadyState(); } $this->scrollIntoViewHoverClick($this->getMenuItemSelectXpathByTitle($menuItem), self::XPATH); $this->pageTitleWait($waitPageTitle); $this->waitUntilDomReadyState(); if (!is_null($waitForElementSelector)) { $this->elementWait($waitForElementSelector, self::CSS_SELECTOR); } } public function getMenuItemSelectXpathByTitle(string $title): string { return "//div[@id='fablesNavDropdown']//a[contains(., '$title')]"; } public function getCompMenuItemSelectXpathByTitle(string $title): string { return "//div[@id='company_menu']//a[contains(., '$title')]"; } protected function assertExpectedElements(array $expectedElements, string $elementSelectorType = null): void { foreach ($expectedElements as $expected) { $this->assertTrue($this->elementExists($expected, $elementSelectorType), "Unable to locate element: $expected"); } } protected function getRowDataByRowId(int $rowId, array $chkBoxes = []): array { $data = []; $cells = $this->webDriver->findElements(WebDriverBy::cssSelector('.table-rows .sticky-header ul.thead > li')); $dataCells = $this->webDriver->findElements(WebDriverBy::cssSelector('.table-rows .tbody ul[id="' . $rowId . '"] > li')); foreach ($cells as $key => $cell) { $cellKey = $cell->getAttribute('id'); if (in_array($cellKey, $chkBoxes)) { $class = $dataCells[$key]->findElement(WebDriverBy::cssSelector('.ncheckbox .status.chk'))->getAttribute('class'); $data[$cellKey] = strpos($class, 'chked') !== false; } else { $data[$cellKey] = trim($dataCells[$key]->getText()); } } return $data; } private function getTableElementIndexBy(string $idToFind, string $selector, string $selectorType = self::ID): ?int { $elements = $this->webDriver->findElements($this->getWebDriverBy($selector, $selectorType)); foreach ($elements as $key => $el) { if ($el->getAttribute('id') == $idToFind) { return $key; } } return null; } protected function selectTableDropDownLiItemById(string $dropDownId, string $dropDownItemId): void { // Open search if closed $this->scrollIntoViewHoverClick('#btnSearch', self::CSS_SELECTOR); $this->scrollIntoViewHoverClick('.table-rows .sticky-header ul.thead > li#' . $dropDownId, self::CSS_SELECTOR, false); $index = $this->getTableElementIndexBy($dropDownId, '.table-rows .sticky-header ul.thead > li', self::CSS_SELECTOR); $this->scrollIntoViewHoverClick('//div[@id="tSearch"]//ul/li[' . ($index + 1) . ']', self::XPATH); $this->scrollIntoViewHoverClick('//div[@id="tSearch"]//ul/li[' . ($index + 1) . ']//ul/li[@id="' . $dropDownItemId . '"]', self::XPATH); } protected function setBrowserDimensions(int $width, int $height): void { $this->webDriver->manage()->window()->setSize(new WebDriverDimension($width, $height)); } /** * @param int $zoomPercentage * @return void */ protected function setBrowserContentZoom(int $zoomPercentage): void { if ($this->seleniumBrowser == 'firefox') { // Firefox $this->webDriver->executeScript( 'document.body.style.MozTransform = "scale(' . $zoomPercentage / 100 . ')";' ); $this->webDriver->executeScript('document.body.style.MozTransformOrigin = "top";'); } else { // Other cases $this->webDriver->executeScript('document.body.style.zoom = "' . $zoomPercentage . '%";'); } } protected function isElementHorizontalScrollbarVisible( string $selector, string $selectorType = self::ID ): bool { $scrollbars = $this->getElementScrollbarVisibility($selector, $selectorType); return $scrollbars['horizontal']; } protected function isElementVerticalScrollbarVisible( string $selector, string $selectorType = self::ID ): bool { $scrollbars = $this->getElementScrollbarVisibility($selector, $selectorType); return $scrollbars['vertical']; } /** @return array */ private function getElementScrollbarVisibility( string $selector, string $selectorType = self::ID ): array { $element = $this->getRemoteWebElement($selector, $selectorType); $script = " let node = arguments[0]; let overflowY = window.getComputedStyle(node)['overflow-y']; let overflowX = window.getComputedStyle(node)['overflow-x']; return { vertical: (overflowY === 'scroll' || overflowY === 'auto') && node.scrollHeight > node.clientHeight, horizontal: (overflowX === 'scroll' || overflowX === 'auto') && node.scrollWidth > node.clientWidth, }; "; $data = (array)$this->webDriver->executeScript($script, [$element]); return [ 'vertical' => isset($data['vertical']) && $data['vertical'] == true, 'horizontal' => isset($data['horizontal']) && $data['horizontal'] == true, ]; } protected function selectDropDownByValue( string $selector, string $value, string $selectorType = self::ID ): void { $this->elementWait($selector, $selectorType, $this->timeoutWait); $time = microtime(true); $totalTime = 0; while ($totalTime < $this->timeoutWait) { try { (new WebDriverSelect($this->getRemoteWebElement($selector, $selectorType)))->selectByValue($value); return; } catch (Exception $exception) { } usleep(250); $totalTime = microtime(true) - $time; } throw new Exception("Select value not found '$value'."); } protected function waitForElementEnabled(string $selector, string $selectorType = self::ID): void { $this->elementWait($selector, $selectorType, $this->timeoutWait); $time = microtime(true); $totalTime = 0; while ($totalTime < $this->timeoutWait) { try { if ($this->getRemoteWebElement($selector, $selectorType)->isEnabled()) { return; } } catch (Exception $exception) { } usleep(250); $totalTime = microtime(true) - $time; } throw new Exception("Element '{$selector}' by '{$selectorType}' is not enabled."); } protected function waitForElementDisplay(string $selector, string $selectorType = self::ID): void { $this->elementWait($selector, $selectorType, $this->timeoutWait); $time = microtime(true); $totalTime = 0; while ($totalTime < $this->timeoutWait) { try { if ($this->getRemoteWebElement($selector, $selectorType)->isDisplayed()) { return; } } catch (Exception $exception) { } usleep(250); $totalTime = microtime(true) - $time; } throw new Exception("Element '{$selector}' by '{$selectorType}' is not displayed."); } /** * @param string $file * @param array $excelValueCfg * @param bool $containsStrings * @return void */ protected function assertionsOnExcelSheetData( string $file = '', array $excelValueCfg = [], bool $containsStrings = false ): void { try { $xmlCheckerObject = new ExcelDataExtractForTestingValues($this->applicationDownloadFolder); $xmlCheckerObject->assertionsOnExcelSheetData($this, $file, $excelValueCfg, $containsStrings); } catch (Exception $e) { $this->fail($e->getMessage()); } } public function moveToElement(string $selector, string $selectorType = self::ID): RemoteWebElement { $element = $this->getVisibleFormFieldElement($selector, $selectorType); $this->webDriver->action()->moveToElement($element)->perform(); return $element; } }