fail("Only supports ID selector type for now."); } return ".doc .form-data #$id"; } public function sendKeysAndTriggerValidation(string $selector, $value, string $selectorType = self::ID): void { // Hover the parent element $containerXpath = $this->getFieldValidateContainerXpath($selector, $selectorType); $this->elementWait($containerXpath, self::XPATH); $this->scrollElementIntoView($containerXpath, self::XPATH); $this->webDriver->action()->moveToElement($this->byXPath($containerXpath))->perform(); try { // IMPORTANT: Pages can have multiple elements with the same id. // Always type into the visible one. $remoteElement = $this->getVisibleFormFieldElement($selector, $selectorType); $classes = $remoteElement->getAttribute('class'); if (str_contains($classes, 'hasDatepicker') || str_contains($classes, 'chz')) { /** Handle datepicker and dropdown differently */ if (str_contains($classes, 'hasDatepicker')) { $this->setDatepickerValue($selector, $value, $selectorType); } if (str_contains($classes, 'chz')) { $this->setDropdownValueWithJquery($selector, $value); } return; } // Clear & update field value $remoteElement->click(); $remoteElement->clear()->sendKeys($value); // Wait for field validation $this->executeJSBlur(); } catch (\Exception $e) { $msg = "\nFailed on element $selectorType: $selector"; $msg .= "\nContainer xpath: $containerXpath"; $msg .= "\nValue: $value"; $msg .= "\nMessage: {$e->getMessage()}"; $this->fail($msg); } $this->waitUntilDomReadyState(); } public function setDatepickerValue(string $selector, $value, string $selectorType = self::ID): void { try { $this->getRemoteWebElement($selector, $selectorType)->click()->clear()->sendKeys($value); $this->executeJSBlur(); } catch (\Exception $e) { $this->fail("Failed on setting Date value on: \nSelector: $selector \nValue: $value \nSelector type: $selectorType"); } } public function setDropdownValueWithJquery(string $id, $value): void { try { // Trigger blur/change to ensure the chosen dropdown update $this->executeJS("$('select#$id').val('$value').trigger('chosen:updated').trigger('change');"); $this->executeJSBlur(); $this->waitUntilDomReadyState(); } catch (\Exception $e) { $this->fail("Failed on selecting dropdown value with jQuery on element id: $id"); } } public function setDropdownValueByIndexWithJquery(string $id, int $index): void { // eq(0) = first, eq(1) = second ... $value = $this->executeJS("return $('select#$id').find('option').eq($index).val();"); $this->setDropdownValueWithJquery($id, $value); } /** * Multi select box */ public function selectItemInMultiSelectBox(string $selector, string $value): void { /** * This widget is a dual multi-select: * - left: select#items_from * - right: select#items_to * - add: a.selector-add * We select option(s) by value on the left and click "Add", * then wait until the option appears on the right. */ try { $this->openFormSection($selector); $rootCss = "#$selector #multiselect_1"; $fromCss = "$rootCss select#items_from"; $toCss = "$rootCss select#items_to"; $addCss = "$rootCss a.selector-add"; $this->scrollElementIntoView($fromCss, self::CSS_SELECTOR); $this->select($this->byCssSelector($fromCss))->selectByValue($value); // Click "Add" (move to the right) $this->scrollIntoViewHoverClick($addCss, self::CSS_SELECTOR); // Wait until the moved option exists in the right select $this->elementWait( $toCss . ' option[value="' . $value . '"]', self::CSS_SELECTOR, true, true, false, false ); $this->executeJSBlur(); } catch (\Exception $e) { $this->fail("Failed on selecting item in multi select box on element id: $selector"); } } public function removeItemInMultiSelectBox(string $selector, string $value): void { /** Remove a single value from the RIGHT box using "Remove". */ try { $this->openFormSection($selector); $rootCss = "#$selector #multiselect_1"; $toCss = "$rootCss select#items_to"; $this->scrollElementIntoView($toCss, self::CSS_SELECTOR); $toSelect = $this->byCssSelector($toCss); // Ensure the option is present before trying to remove (clear error message if not) if (!$this->elementExists($toCss . ' option[value="' . $value . '"]', self::CSS_SELECTOR, true)) { $this->fail("Value '$value' not found in selected multi select box."); } $this->select($toSelect)->selectByValue($value); $this->scrollIntoViewHoverClick("$rootCss a.selector-remove", self::CSS_SELECTOR); $this->executeJSBlur(); } catch (\Exception $e) { $this->fail("Failed on removing item in multi select box on element id: $selector"); } } public function chooseAllInMultiSelectBox(string $selector): void { try { $this->openFormSection($selector); $this->scrollIntoViewHoverClick("#$selector #multiselect_1 .available a.chooseall", self::CSS_SELECTOR); $this->executeJSBlur(); } catch (\Exception $e) { $this->fail("Failed on choose-all in multi select box on element id: $selector"); } } public function clearAllInMultiSelectBox(string $selector): void { try { $this->openFormSection($selector); $this->scrollIntoViewHoverClick("#$selector #multiselect_1 .chosen a.clearall", self::CSS_SELECTOR); $this->executeJSBlur(); } catch (\Exception $e) { $this->fail("Failed on clear-all in multi select box on element id: $selector"); } } private function getMultiSelectChosenValues(string $selector): array { $selector = "#$selector #multiselect_1 select#items_to option"; if (!$this->elementExists($selector, self::CSS_SELECTOR, true)) { return []; } $options = $this->getRemoteWebElements($selector, self::CSS_SELECTOR); $values = []; foreach ($options as $opt) { $values[] = (string)$opt->getAttribute('value'); } return $values; } public function assertMultiSelectChosenContains(string $selector, array $expected): void { $actual = $this->getMultiSelectChosenValues($selector); $missing = array_values(array_diff($expected, $actual)); $this->assertEmpty( $missing, 'Multiselect is missing expected values: ' . implode(', ', $missing) . '. Actual: ' . implode(', ', $actual) ); } public function assertMultiSelectChosenEquals(string $selector, array $expected): void { $actual = $this->getMultiSelectChosenValues($selector); $this->assertEquals( $expected, $actual, 'Multiselect chosen values mismatch. Expected: ' . implode(', ', $expected) . '. Actual: ' . implode(', ', $actual) ); } /** * Form Settings */ public function openFormSection(string $selector = 'psettings'): void { $settings = $this->byId($selector); if (!strpos($settings->getAttribute('class'), 'hide')) { $settings->click(); //$this->waitUntilDomReadyState(); } } public function closeFormSection(string $selector = 'psettings'): void { $settings = $this->byId($selector); if (strpos($settings->getAttribute('class'), 'hide')) { $settings->click(); //$this->waitUntilDomReadyState(); } } private function addNewDataRowInFormSection(string $settings) { $this->byCssSelector("#$settings > div > div.add_more > a")->click(); $this->waitUntilDomReadyState(); } /** * Form actions */ public function cancelMainForm(): void { $this->scrollIntoViewHoverClick('#base_table > div.doc div.title > div.fables-light-background-color > div > a', self::CSS_SELECTOR); $this->waitUntilDomReadyState(); } public function goBackToTable(): void { $this->scrollIntoViewHoverClick('.buttons_bottom #tback', self::CSS_SELECTOR); $this->waitUntilDomReadyState(); } public function saveMainForm(): void { $this->scrollIntoViewHoverClick('.buttons_bottom #fsave', self::CSS_SELECTOR); $this->waitUntilDomReadyState(); } public function saveMainFormAndAssertSuccess(string $lang): void { $this->saveMainForm(); $this->assertFormSuccessfullySavedMessage($lang); } /** * Quick generate data for errors and save */ private function createNewDataRowsTestErrorsAndFillWithData(int $rows = 1, array $data = [], array $cfg = []): void { for ($row = 1; $row <= $rows; $row++) { $this->createNewDataRowTestErrorsAndFillForSave($data, $cfg); } } private function createNewDataRowTestErrorsAndFillForSave(array $testData, array $rowCfg = []): void { if (empty($testData) || empty($rowCfg)) { return; } $elements = $this->getRemoteWebElements("{$rowCfg['container']} > ul", self::CSS_SELECTOR); $this->byCssSelector($rowCfg['newRow'])->click(); $newIndex = (count($elements) + 1); // Wait for the row to pop up $this->elementWait($rowCfg['rowKey'] . $newIndex); foreach ($testData as $field) { // Change fields unique id in found a key $field['v'] = str_replace('%ID%', $newIndex, $field['v']); $id = $field['id'] . $newIndex; $fieldPath = "{$rowCfg['inputPath']}#$id"; $fieldParentPath = str_replace('%FIELD%', $id, $rowCfg['inputXPath']); $ribbonErrorSelector = "div.custom-status .cdrop [data-id='$id'] .emsg"; if ($field['mandatory'] ?? false) { $this->webDriver->executeScript("$('$fieldPath').focus().blur();"); // Wait for the error $this->elementWait("{$fieldParentPath}[contains(@class,'v-error')]", self::XPATH); $this->elementWait($ribbonErrorSelector, self::CSS_SELECTOR); // Check top ribbon error text $actualText = $this->byCssSelector($ribbonErrorSelector)->getText(); $this->assertEquals(($field['o_text']), ($actualText)); $this->byCssSelector($fieldPath)->sendKeys($field['v']); $this->webDriver->executeScript("$('$fieldPath').blur();"); $this->elementWaitRemoved("{$fieldParentPath}[contains(@class,'v-error')]", self::XPATH); $this->elementWaitRemoved($ribbonErrorSelector, self::CSS_SELECTOR); } elseif ($field['v'] ?? false) { $this->byCssSelector($fieldPath)->clear(); $this->byCssSelector($fieldPath)->sendKeys($field['v']); $this->webDriver->executeScript("$('$fieldPath').focus().blur();"); $this->elementWait("{$fieldParentPath}[contains(@class,'v-ok')]", self::XPATH); $this->assertTrue( $this->elementExists("{$fieldParentPath}[contains(@class,'v-ok')]", self::XPATH) ); } } } }