div.table-data'; private $colHeaderSelector = '#cms-table-drop-marker ul.thead'; private $tableBodyContainer = '#cms-table-drop-marker .table-rows .tbody'; private $tablePagerContainer = "//ul[@class='dx pager-cnt']"; public function testTableHeaderTitles(array $idToTitleText = null): void { if (is_null($idToTitleText)) { return; } foreach ($idToTitleText as $id => $textToValidate) { $this->assertTrue( $this->elementExists("$this->colHeaderSelector #$id", self::CSS_SELECTOR), "Failed locating element by CSS selector: $this->colHeaderSelector #$id" ); switch ($id) { case 'h_checkbox': break; case 'h_buttons': $this->assertStringContainsString( $textToValidate, $this->byCssSelector("$this->colHeaderSelector #$id > a")->getText() ); break; default: $this->assertEquals( $textToValidate, $this->byCssSelector("$this->colHeaderSelector #$id div")->getText() ); break; } } } public function testTableRowBaseCheckboxes(): void {} public function testTableBaseFunctionality() { // Test header columns // Test column sort // Test column reorder // Test rows per page // Test column visibility // Test row search } /** Table settings */ public function openTableSettings(string $id = 'iTableSettings', string $class = 'base-table-settings'): void { $headerSelector = "#dialogSlider.$class .slider-wrapper .header > h3"; if ( $this->elementExists($headerSelector, self::CSS_SELECTOR, true) && $this->byCssSelector($headerSelector)->isDisplayed() ) { return; } $this->selectTableActionById($id); $this->waitTableSlider($headerSelector); } public function closeTableSettings(): void { $headerSelector = '#dialogSlider .slider-wrapper .header > h3'; if ( $this->elementExists($headerSelector, self::CSS_SELECTOR) && $this->byCssSelector($headerSelector)->isDisplayed() ) { $this->scrollElementIntoView($headerSelector, self::CSS_SELECTOR); $this->byCssSelector("$headerSelector a.close")->click(); $this->elementWaitRemoved("$headerSelector a.close", self::CSS_SELECTOR); } } public function updateTableSettingsViewMaxRows(int $rows = 40): void { $this->openTableSettings(); $this->setTableSettingsMaxRowsPerPage($rows); $this->cmsConfirmDialogSliderActionYes(); $this->waitUntilDomReadyState(); $rowsListed = $this->getRemoteWebElements("$this->tableBodyContainer > ul", self::CSS_SELECTOR); $this->assertTrue(count($rowsListed) <= $rows); } public function setTableSettingsMaxRowsPerPage(int $rowsPerPage = 10): void { $this->openTableSettings(); $inputSelector = '#dialogSlider .slider-wrapper #pBase #a_rec_per_page'; $this->scrollElementIntoView($inputSelector, self::CSS_SELECTOR); $this->byCssSelector($inputSelector)->clear()->sendKeys($rowsPerPage); $this->webDriver->executeScript('$("' . $inputSelector . '").focus().blur();'); } public function selectTableSettingsCheckbox(string $id, bool $visibility): void { $chkSelector = "#dialogSlider .slider-wrapper .document-settings .inner-cnt > ul #$id"; $this->scrollElementIntoView($chkSelector, self::CSS_SELECTOR); $element = $this->byCssSelector($chkSelector); $isChecked = str_contains($element->getAttribute('class'), "chked"); if ($isChecked !== $visibility) { $element->click(); } } public function selectTableRow(int $row): void { $chkSelector = $this->tableBodyContainer . " > ul:nth-child($row) > li.first > div > div.chk"; $this->toggleCheckbox($chkSelector); } public function resetTableSettingsColumnConfig(array $defaultConfig): void { $this->openTableSettings(); $this->scrollIntoViewHoverClick("#dialogSlider .slider-wrapper #reset_cfg", self::CSS_SELECTOR); $this->waitUntilDomReadyState(); $this->elementWaitRemoved("#dialogSlider .slider-wrapper a.close", self::CSS_SELECTOR); $this->testTableHeaderTitles($defaultConfig); } public function updateTableSettingsColumnsVisibility(array $config): void { $this->openTableSettings(); foreach ($config as $id => $data) { $this->selectTableSettingsCheckbox($id, $data); } $this->cmsConfirmDialogSliderActionYes(); $this->waitUntilDomReadyState(); foreach ($config as $id => $visibility) { $elementExists = $this->elementExists("$this->colHeaderSelector > #$id", self::CSS_SELECTOR, true); $this->assertEquals($visibility, $elementExists, "Element id: $id"); } } public function reorderTableSettingsColumnConfig(array $orderedIds): void { $this->openTableSettings(); $allItemsSel = '#dialogSlider .slider-wrapper .document-settings .inner-cnt li'; $allItems = $this->getRemoteWebElements($allItemsSel, self::CSS_SELECTOR); foreach ($orderedIds as $targetPosition => $itemId) { $elementXpath = "//div[@id='dialogSlider']/div[2]//ul/li[.//*[@id='$itemId']]"; $targetPosition += 1; // First is reserved $this->dragTableSettingListItemToPosition($elementXpath, $allItems, $targetPosition); } $this->cmsConfirmDialogSliderActionYes(); foreach ($orderedIds as $itemId) { $exists = $this->elementExists("$this->colHeaderSelector > #$itemId", self::CSS_SELECTOR, true); $this->assertTrue($exists, 'Failed on: ' . $itemId); } } public function reorderTableColumn(array $orderedIds): void { foreach ($orderedIds as $targetPosition => $itemId) { $elementXpath = "//*[@id='cms-table-drop-marker']//li[@id='$itemId']"; $allItemsSel = $this->colHeaderSelector . ' > li'; $allItems = $this->getRemoteWebElements($allItemsSel, self::CSS_SELECTOR); $targetPosition += 2; $this->dragTableSettingListItemToPosition($elementXpath, $allItems, $targetPosition); } foreach ($orderedIds as $itemId) { $exists = $this->elementExists("$this->colHeaderSelector > #$itemId", self::CSS_SELECTOR, true); $this->assertTrue($exists, 'Failed on: ' . $itemId); } } public function dragTableSettingListItemToPosition(string $xpath, array $allItems, int $targetPosition): void { $sourceElement = $this->scrollElementIntoView($xpath, self::XPATH); $currentPosition = $this->getElementPosition($sourceElement, $allItems); if ($currentPosition === $targetPosition) { return; } $targetElement = $allItems[$targetPosition]; $movingLeft = $currentPosition > $targetPosition; // Target near edge of the target element — avoids misfiring on wide columns // Offset is from the center: negative = left side, positive = right side $targetWidth = $targetElement->getSize()->getWidth(); $xOffset = $movingLeft ? -intdiv($targetWidth, 2) + 5 // near left edge : intdiv($targetWidth, 2) - 5; // near right edge try { $this->webDriver->action() ->clickAndHold($sourceElement) ->moveToElement($targetElement, $xOffset, 0) ->release() ->perform(); $this->waitUntilDomReadyState(); } catch (WebDriverException $e) { $this->assertFalse( sprintf( "dragListItemToPosition failed.\nSource xpath: %s\nError: %s", $xpath, $e->getMessage() ) ); } } public function waitTableSlider($headerSelector): void { $this->elementWait($headerSelector, self::CSS_SELECTOR); $this->scrollElementIntoView($headerSelector, self::CSS_SELECTOR); $exists = $this->elementExists($headerSelector, self::CSS_SELECTOR, true); $this->assertTrue($exists, "Failed locating element by CSS selector: $headerSelector"); } /** Table actions */ public function selectTableActionById($id): void { $this->scrollIntoViewHoverClick("$this->colHeaderSelector #h_buttons a", self::CSS_SELECTOR); $this->scrollIntoViewHoverClick("#table-actions li#$id a", self::CSS_SELECTOR); $this->waitUntilDomReadyState(); } public function deleteTableRow($row, bool $scrollIntoView = false): void { $selector = $this->tableBodyContainer . " > ul:nth-child($row) > li.last.actions > div > a#ldelete"; $this->openConfirmDialog($selector, $scrollIntoView); $this->cmsConfirmDialogActionYes(); } public function editTableRow($row, bool $scrollIntoView = false): void { $selector = $this->tableBodyContainer . " > ul:nth-child($row) > li.last.actions > div > a#ledit"; if ($scrollIntoView) { $this->scrollElementIntoView($selector, self::CSS_SELECTOR); } $this->byCssSelector($selector)->click(); $this->waitUntilDomReadyState(); } public function editTableRowById(int $id, bool $scrollIntoView = false): void { $selector = $this->tableBodyContainer . " ul[id='$id'] > li.last.actions > div > a#ledit"; if ($scrollIntoView) { $this->scrollElementIntoView($selector, self::CSS_SELECTOR); } $this->byCssSelector($selector)->click(); $this->waitUntilDomReadyState(); } public function clickRowExcelExport($row, bool $scrollIntoView = false): void { $selector = $this->tableBodyContainer . " > ul:nth-child($row) > li > div > a.download_pdf"; if ($scrollIntoView) { $this->scrollElementIntoView($selector, self::CSS_SELECTOR); } $this->byCssSelector($selector)->click(); $this->waitUntilDomReadyState(); } public function clickRowExcelExportById(int $id, bool $scrollIntoView = false): void { $selector = $this->tableBodyContainer . " > ul[id='$id'] > li > div > a.download_pdf"; if ($scrollIntoView) { $this->scrollElementIntoView($selector, self::CSS_SELECTOR); } $this->byCssSelector($selector)->click(); $this->waitUntilDomReadyState(); } public function addNewRecord(): void { $this->scrollIntoViewHoverClick('.table-bactions ' . $this->getNewButtonId(), self::CSS_SELECTOR); $this->waitUntilDomReadyState(); } public function getNewButtonId(): string { return '#bButtonNew'; } public function cloneTableRow(int $row): void { $this->selectTableRow($row); $this->byId('clone')->click(); $this->waitUntilDomReadyState(); } /** Table search */ public function openCmsTableSearch(): void { $this->byId("btnSearch")->click(); } public function resetCmsTableSearch(): void { $this->byCssSelector(".sticky-header .search-hidden #tSearch > ul #tclear")->click(); $this->waitUntilDomReadyState(); } public function searchCmsTableByColumn(string $columnID, string $value, bool $submit = true): void { if (!$this->getRemoteWebElement('tSearch')->isDisplayed()) { $this->openCmsTableSearch(); } $search = ".sticky-header .search-hidden #tSearch > ul"; $selector = "$search #$columnID"; $parentLi = $this->byCssSelector($selector)->findElement(WebDriverBy::xpath("./ancestor::li[1]")); if (strpos($parentLi->getAttribute('class'), 'dropBox') !== false) { $this->scrollIntoViewHoverClick("//input[@id='$columnID']/preceding-sibling::span[1]", self::XPATH); $selectedValue = "//input[@id='$columnID']/ancestor::li//ul/li[@id='$value']"; $this->scrollIntoViewHoverClick($selectedValue, self::XPATH); $this->waitUntilDomReadyState(); // We don't need to submit the search as DropBox auto submits the form. } else { $element = $this->byCssSelector($selector); $element->click()->clear()->sendKeys($value); if ($submit) { $element->click()->sendKeys(WebDriverKeys::ENTER); $this->waitUntilDomReadyState(); } } } public function sortTableByColumn(string $columnID, string $orderWay = 'ASC'): void { $colSelector = "$this->colHeaderSelector #$columnID"; $this->assertTrue( $this->elementExists($colSelector, self::CSS_SELECTOR), "Failed locating element by CSS selector: $this->colHeaderSelector #$columnID" ); if ( $orderWay == 'ASC' && ( $this->elementHasClass('sortDesc', $colSelector, self::CSS_SELECTOR) || !$this->elementHasClass('sort', $colSelector, self::CSS_SELECTOR) ) ) { $this->byCssSelector($colSelector)->click(); $this->elementWait("$this->colHeaderSelector #$columnID.sort", self::CSS_SELECTOR); } if ( $orderWay == 'DESC' && !$this->elementHasClass('sortDesc', $colSelector, self::CSS_SELECTOR) ) { if (!$this->elementHasClass('sort', $colSelector, self::CSS_SELECTOR)) { $this->byCssSelector($colSelector)->click(); $this->elementWait("$this->colHeaderSelector #$columnID.sort", self::CSS_SELECTOR); } $this->byCssSelector($colSelector)->click(); $this->elementWait("$this->colHeaderSelector #$columnID.sortDesc", self::CSS_SELECTOR); } } /** Footer helper functions **/ public function navigateThroughPager($expectedPages, $pageCallback = null, int $fix = 7) { $this->waitUntilDomReadyState(); $this->assertCurrentPage(1); $this->assertPreviousButtonExists(false); // If only 1 page, then run the callback and go back. if ($expectedPages === 1) { $this->assertNextButtonExists(false); if ($pageCallback) { call_user_func($pageCallback, 1); } return; } $this->assertNextButtonExists($this->shouldShowNextButton(1, $expectedPages)); // Navigate through each page - test the next button. for ($page = 2; $page <= $expectedPages; $page++) { if (!$this->shouldShowNextButton($page, $expectedPages)) { $this->clickPageNumber($page); } else { $this->clickNext(); } $this->waitUntilDomReadyState(); $this->assertCurrentPage($page); $this->assertPreviousButtonExists($this->shouldShowPreviousButton($page, $expectedPages, $fix)); $this->assertNextButtonExists($this->shouldShowNextButton($page, $expectedPages, $fix)); if ($pageCallback) { call_user_func($pageCallback, $page); } } // Go back to page 1 - test the previous button for ($page = $expectedPages - 1; $page >= 1; $page--) { if ($this->shouldShowPreviousButton($page, $expectedPages,$fix)) { $this->clickPrevious(); } else { $this->clickPageNumber($page); } $this->waitUntilDomReadyState(); $this->assertCurrentPage($page); $this->assertPreviousButtonExists($this->shouldShowPreviousButton($page, $expectedPages,$fix)); $this->assertNextButtonExists($this->shouldShowNextButton($page, $expectedPages,$fix)); } } public function shouldShowPreviousButton($currentPage, $totalPages, int $fix = 7): bool { /** Previous is displayed only if total pages over 7, and we are over the fifth page. */ return $totalPages > $fix && $currentPage > 4; } public function shouldShowNextButton($currentPage, $totalPages, int $fix = 7): bool { /** The Next button is displayed if there are more than 7 pages, and we're not on the last 4 pages. */ return $totalPages > $fix && $currentPage < $totalPages - 3; } public function clickNext(): void { $this->scrollIntoViewHoverClick( "$this->tablePagerContainer/li[contains(@class, 'last')]//a[@class='nextp']", self::XPATH, ); } public function clickPrevious(): void { $this->scrollIntoViewHoverClick( "$this->tablePagerContainer/li[contains(@class, 'first')]//a[@class='prevp']", self::XPATH, ); } public function clickPageNumber($pageNumber): void { $this->scrollIntoViewHoverClick("$this->tablePagerContainer/li[@id='$pageNumber']//a", self::XPATH); } public function assertPreviousButtonExists(bool $exists = true): void { $selector = "$this->tablePagerContainer/li[contains(@class, 'first')]//a[@class='prevp']"; $this->assertSame($exists, $this->elementExists($selector, self::XPATH, true)); } public function assertNextButtonExists(bool $exists = true): void { $selector = "$this->tablePagerContainer/li[contains(@class, 'last')]//a[@class='nextp']"; $this->assertSame($exists, $this->elementExists($selector, self::XPATH, true)); } public function assertCurrentPage($pageNumber): void { $selectedPage = $this->byXPath("$this->tablePagerContainer/li[contains(@class, 'sel')]"); $actualPage = (int)$selectedPage->getAttribute('id'); $this->assertEquals($pageNumber, $actualPage, "Wrong page. Expected: $pageNumber, actual: $actualPage"); // Check has 'selected' $link = $selectedPage->findElement(WebDriverBy::tagName('a')); $msg = "Page $pageNumber link has not been marked as selected"; $this->assertStringContainsString('selected', $link->getAttribute('class'), $msg); } public function assertTablePagerCount(string $pager): void { $selector = "#cms-table-drop-marker .pager .rcount"; $this->assertStringContainsString($pager, $this->byCssSelector($selector)->getText()); } public function toggleTableRowLastCheckbox(int $id, bool $tick = true): void { $chkSelector = "//ul[@id='$id']//li[not(contains(@class, 'first'))][last()]//div[@class='ncheckbox']"; $this->scrollElementIntoView($chkSelector, self::XPATH); $element = $this->byXPath($chkSelector); $isChecked = str_contains($element->getAttribute('class'), "chked"); if ($isChecked !== $tick) { $element->click(); $this->waitUntilDomReadyState(); } } public function searchCmsTableByColumnId(int $id, bool $submit = true): void { $this->searchCmsTableByColumn('id', (string) $id, $submit); } public function sortCmsTableByColumnId(string $order = 'ASC'): void { $this->sortTableByColumn('h_id', $order); } public function validateTableOrderBy($page, array $expectedData): void { $col = 3; foreach ($page->tableColumns as $tableColumn) { if (in_array($tableColumn, $page->tableColumnsToSkip ?? [])) { continue; } if (!isset($expectedData[$tableColumn])) { $col++; continue; } $this->sortTableByColumn($tableColumn); $this->assertCmsTableRowData(1, $col, $expectedData[$tableColumn]['ASC']); $this->sortTableByColumn($tableColumn, 'DESC'); $this->assertCmsTableRowData(1, $col, $expectedData[$tableColumn]['DESC']); $col++; } } }