diff --git a/src/AdminCabinet/Controllers/FirewallController.php b/src/AdminCabinet/Controllers/FirewallController.php
index a44198dfc..e12f34d1a 100644
--- a/src/AdminCabinet/Controllers/FirewallController.php
+++ b/src/AdminCabinet/Controllers/FirewallController.php
@@ -432,22 +432,26 @@ public function disableAction(): void
}
/**
- * Sort array. The localnet and 0.0.0.0 should be at the first position on the list
+ * Compare two network entries for sorting
*
- * @param $a
- * @param $b
- *
- * @return bool
+ * @param array $a First network entry
+ * @param array $b Second network entry
+ * @return int Returns -1 if $a should be placed before $b,
+ * 1 if $a should be placed after $b,
+ * 0 if they are considered equal
*/
- private function sortArrayByNetwork($a, $b): bool
+ private function sortArrayByNetwork(array $a, array $b): int
{
+ // If second entry is permanent and first is not 0.0.0.0/0
if ($b['permanent'] && $a['network'] !== '0.0.0.0/0') {
- return true;
+ return 1; // Move $a after $b
}
+
+ // If second entry is 0.0.0.0/0
if ($b['network'] === '0.0.0.0/0') {
- return true;
+ return 1; // Move $a after $b
}
- return false;
+ return -1; // In all other cases, move $a before $b
}
}
diff --git a/src/AdminCabinet/Views/IncomingRoutes/modify.volt b/src/AdminCabinet/Views/IncomingRoutes/modify.volt
index ac73f21ea..e3e591c4f 100644
--- a/src/AdminCabinet/Views/IncomingRoutes/modify.volt
+++ b/src/AdminCabinet/Views/IncomingRoutes/modify.volt
@@ -26,7 +26,7 @@
{{ partial("partials/playAddNewSound", ['label': t._('iv_PlaySound'), 'id':'audio_message_id', 'fieldClass':'eleven wide field', 'fieldId':'']) }}
{{ t._("ir_CallTransferTo") }}
-
+
{{ form.render('extension') }}
diff --git a/src/PBXCoreREST/Middleware/AuthenticationMiddleware.php b/src/PBXCoreREST/Middleware/AuthenticationMiddleware.php
index 884a5113d..5cebe8ce4 100644
--- a/src/PBXCoreREST/Middleware/AuthenticationMiddleware.php
+++ b/src/PBXCoreREST/Middleware/AuthenticationMiddleware.php
@@ -1,4 +1,5 @@
isLocalHostRequest()
- && true !== $request->isAllowedAction($application)) {
+ if (
+ true !== $isNoAuthApi
+ && true !== $request->isLocalHostRequest()
+ && true !== $request->isAllowedAction($application)
+ ) {
$this->halt(
$application,
- $response::FORBIDDEN,
- 'The route is not allowed'
- );
+ $response::FORBIDDEN,
+ 'The route is not allowed'
+ );
return false;
}
return true;
}
-
-}
\ No newline at end of file
+}
diff --git a/tests/AdminCabinet/Lib/BrowserStackTest.php b/tests/AdminCabinet/Lib/BrowserStackTest.php
index 7e43e86ea..8623fc7a2 100644
--- a/tests/AdminCabinet/Lib/BrowserStackTest.php
+++ b/tests/AdminCabinet/Lib/BrowserStackTest.php
@@ -1,4 +1,5 @@
.
*/
namespace MikoPBX\Tests\AdminCabinet\Lib;
use Facebook\WebDriver\Remote\RemoteWebDriver;
+use Facebook\WebDriver\WebDriverBy;
+use Facebook\WebDriver\WebDriverExpectedCondition;
+use Facebook\WebDriver\WebDriverWait;
+use GuzzleHttp\Client as GuzzleHttpClient;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\TestCase;
use BrowserStack\Local as BrowserStackLocal;
-use GuzzleHttp\Client as GuzzleHttpClient;
+use RuntimeException;
require_once 'globals.php';
/**
- * Class BrowserStackTest
+ * Base class for BrowserStack integration tests
+ *
* @package MikoPBX\Tests\AdminCabinet\Lib
*/
-class BrowserStackTest extends TestCase
+abstract class BrowserStackTest extends TestCase
{
- /**
- * @var RemoteWebDriver
- */
- protected static RemoteWebDriver $driver;
+ protected const WAIT_TIMEOUT = 30;
+ protected const DEFAULT_SCREENSHOT_DIR = 'test-screenshots';
- /**
- * @var BrowserStackLocal
- */
+ protected static RemoteWebDriver $driver;
protected static BrowserStackLocal $bs_local;
+ protected static bool $testResult;
+ protected static array $failureConditions;
+ protected static GuzzleHttpClient $httpClient;
/**
- * @var bool
+ * Configure and start BrowserStack session
+ *
+ * @throws \BrowserStack\LocalException
*/
- protected static bool $testResult;
+ public static function setUpBeforeClass(): void
+ {
+ self::initializeHttpClient();
+ self::setupBrowserStackCapabilities();
+ self::initializeTestState();
+ }
/**
- * @var array
+ * Initialize HTTP client for API requests
*/
- protected static array $failureConditions;
+ private static function initializeHttpClient(): void
+ {
+ self::$httpClient = new GuzzleHttpClient([
+ 'base_uri' => 'https://api.browserstack.com',
+ 'auth' => [
+ $GLOBALS['BROWSERSTACK_USERNAME'],
+ $GLOBALS['BROWSERSTACK_ACCESS_KEY']
+ ]
+ ]);
+ }
/**
- * Set up before all tests
+ * Setup BrowserStack capabilities and start session
*
* @throws \BrowserStack\LocalException
*/
- public static function setUpBeforeClass(): void
+ private static function setupBrowserStackCapabilities(): void
{
- // Load the global configuration array
- $CONFIG = $GLOBALS['CONFIG'];
+ $CONFIG = $GLOBALS['CONFIG'];
+ $taskId = (int)getenv('TASK_ID') ?: 0;
- // Get the current task ID from an environment variable, or use 0 if the environment variable is not set
- $task_id = getenv('TASK_ID') ? getenv('TASK_ID') : 0;
+ $caps = self::mergeBrowserStackCapabilities($CONFIG, $taskId);
+ self::initializeBrowserStackLocal($caps);
- // Get the capabilities for the current task from the configuration array
- $caps = $CONFIG['environments'][$task_id];
+ $url = sprintf(
+ 'https://%s:%s@%s/wd/hub',
+ $GLOBALS['BROWSERSTACK_USERNAME'],
+ $GLOBALS['BROWSERSTACK_ACCESS_KEY'],
+ $CONFIG['server']
+ );
+
+ self::$driver = RemoteWebDriver::create($url, $caps, 120000, 120000);
+ }
- // Loop through all the capabilities defined in the configuration array
- foreach ($CONFIG["capabilities"] as $key => $value) {
- // If the capability is not already set in the current task's capabilities, add it
- if ( ! array_key_exists($key, $caps)) {
+ /**
+ * Merge BrowserStack capabilities
+ *
+ * @param array $config Configuration array
+ * @param int $taskId Task identifier
+ * @return array
+ */
+ private static function mergeBrowserStackCapabilities(array $config, int $taskId): array
+ {
+ $caps = $config['environments'][$taskId];
+ foreach ($config['capabilities'] as $key => $value) {
+ if (!array_key_exists($key, $caps)) {
$caps[$key] = $value;
}
}
+ $caps['build'] = $GLOBALS['BUILD_NUMBER'];
+ return $caps;
+ }
- // If BrowserStack Local is enabled, start a BrowserStackLocal instance
- if($GLOBALS['BROWSERSTACK_DAEMON_STARTED']==='false')
- {
+ /**
+ * Initialize BrowserStack Local testing
+ *
+ * @param array $caps Capabilities array
+ * @throws \BrowserStack\LocalException
+ */
+ private static function initializeBrowserStackLocal(array &$caps): void
+ {
+ if ($GLOBALS['BROWSERSTACK_DAEMON_STARTED'] === 'false') {
$bs_local_args = [
- "key" => $GLOBALS['BROWSERSTACK_ACCESS_KEY'],
- "localIdentifier" => "".$GLOBALS['bs_localIdentifier']
+ 'key' => $GLOBALS['BROWSERSTACK_ACCESS_KEY'],
+ 'localIdentifier' => (string)$GLOBALS['bs_localIdentifier']
];
self::$bs_local = new BrowserStackLocal();
self::$bs_local->start($bs_local_args);
+ } else {
+ $caps['browserstack.local'] = (string)$GLOBALS['bs_local'];
+ $caps['browserstack.localIdentifier'] = (string)$GLOBALS['bs_localIdentifier'];
}
- // If BrowserStack Local is not enabled, set the BrowserStack Local capability values to the global variables
- else {
- $caps['browserstack.local'] = "".$GLOBALS['bs_local'];
- $caps['browserstack.localIdentifier']="".$GLOBALS['bs_localIdentifier'];
- }
-
- // Set the URL for the BrowserStack WebDriver endpoint
- $url = "https://" . $GLOBALS['BROWSERSTACK_USERNAME'] . ":" . $GLOBALS['BROWSERSTACK_ACCESS_KEY'] . "@" . $CONFIG['server'] . "/wd/hub";
-
- // Set the build capabilities
- $caps['build'] = $GLOBALS['BUILD_NUMBER'];
-
- // Create a new WebDriver instance with the specified URL and capabilities
- self::$driver = RemoteWebDriver::create($url, $caps, 120000, 120000);
+ }
- // Set the initial test result and failure conditions variables
+ /**
+ * Initialize test state variables
+ */
+ private static function initializeTestState(): void
+ {
self::$testResult = true;
self::$failureConditions = [];
}
-
/**
* Set up before each test
*
* @throws GuzzleException
- * @throws \Exception
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
- $sessionID = self::$driver->getSessionID();
- $name = $this->getName(true);
+ $this->updateTestSessionName();
+ $this->prepareTestEnvironment();
+ }
- $client = new GuzzleHttpClient();
- $client->request('PUT', "https://api.browserstack.com/automate/sessions/{$sessionID}.json", [
- 'auth' => [$GLOBALS['BROWSERSTACK_USERNAME'], $GLOBALS['BROWSERSTACK_ACCESS_KEY']],
- 'json' => ['name' => $name]
+ /**
+ * Update test session name in BrowserStack
+ *
+ * @throws GuzzleException
+ */
+ private function updateTestSessionName(): void
+ {
+ $sessionId = self::$driver->getSessionID();
+ self::$httpClient->request('PUT', "/automate/sessions/{$sessionId}.json", [
+ 'json' => ['name' => $this->getName(true)]
]);
+ }
- // Maximize Browser size
+ /**
+ * Prepare test environment
+ */
+ private function prepareTestEnvironment(): void
+ {
self::$driver->manage()->window()->maximize();
-
- // Go to the index page
self::$driver->get($GLOBALS['SERVER_PBX']);
}
+ /**
+ * Helper method to wait for element
+ *
+ * @param string $selector CSS selector
+ * @param int $timeout Timeout in seconds
+ * @return mixed
+ */
+ protected function waitForElement(string $selector, int $timeout = self::WAIT_TIMEOUT)
+ {
+ $wait = new WebDriverWait(self::$driver, $timeout);
+ return $wait->until(
+ WebDriverExpectedCondition::presenceOfElementLocated(
+ WebDriverBy::cssSelector($selector)
+ )
+ );
+ }
+
+ /**
+ * Take screenshot of current page
+ *
+ * @param string $name Screenshot name
+ * @return string Screenshot path
+ */
+ protected function takeScreenshot(string $name): string
+ {
+ $screenshotDir = self::DEFAULT_SCREENSHOT_DIR;
+ if (!is_dir($screenshotDir) && !mkdir($screenshotDir, 0777, true)) {
+ throw new RuntimeException("Failed to create screenshot directory");
+ }
+
+ $path = sprintf('%s/%s_%s.png', $screenshotDir, date('Y-m-d_H-i-s'), $name);
+ self::$driver->takeScreenshot($path);
+ return $path;
+ }
+
/**
* Tear down after each test
*/
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
- if ($this->getStatus()!==0){
+ if ($this->getStatus() !== 0) {
self::$testResult = false;
- self::$failureConditions[] = 'Test: '.$this->getName(true).' Message:'. $this->getStatusMessage();
+ $screenshotPath = $this->takeScreenshot($this->getName());
+ self::$failureConditions[] = sprintf(
+ 'Test: %s Message: %s Screenshot: %s',
+ $this->getName(true),
+ $this->getStatusMessage(),
+ $screenshotPath
+ );
}
}
@@ -150,22 +230,39 @@ public function tearDown(): void
*/
public static function tearDownAfterClass(): void
{
- $client = new GuzzleHttpClient();
- $sessionID = self::$driver->getSessionID();
- $status = self::$testResult?'passed':'failed';
- $statusMessage = implode(PHP_EOL, self::$failureConditions);
- $client->request('PUT', "https://api.browserstack.com/automate/sessions/{$sessionID}.json", [
- 'auth' => [$GLOBALS['BROWSERSTACK_USERNAME'], $GLOBALS['BROWSERSTACK_ACCESS_KEY']],
- 'json' => [
- 'status' => $status,
- 'reason' => $statusMessage,
- ]
- ]);
+ self::updateTestSessionStatus();
+ self::cleanupResources();
+ }
+ /**
+ * Update test session status in BrowserStack
+ */
+ private static function updateTestSessionStatus(): void
+ {
+ try {
+ $sessionId = self::$driver->getSessionID();
+ $status = self::$testResult ? 'passed' : 'failed';
+ $statusMessage = implode(PHP_EOL, self::$failureConditions);
+
+ self::$httpClient->request('PUT', "/automate/sessions/{$sessionId}.json", [
+ 'json' => [
+ 'status' => $status,
+ 'reason' => $statusMessage,
+ ]
+ ]);
+ } catch (GuzzleException $e) {
+ error_log("Failed to update test session status: " . $e->getMessage());
+ }
+ }
+
+ /**
+ * Cleanup test resources
+ */
+ private static function cleanupResources(): void
+ {
self::$driver->quit();
if (isset(self::$bs_local) && self::$bs_local) {
self::$bs_local->stop();
}
}
-
-}
+}
\ No newline at end of file
diff --git a/tests/AdminCabinet/Lib/MikoPBXTestsBase.php b/tests/AdminCabinet/Lib/MikoPBXTestsBase.php
index 0c78d0523..e776373d9 100644
--- a/tests/AdminCabinet/Lib/MikoPBXTestsBase.php
+++ b/tests/AdminCabinet/Lib/MikoPBXTestsBase.php
@@ -1,4 +1,5 @@
findElements(WebDriverBy::xpath($xpath));
- foreach ($selectedExtensions as $element) {
- $currentValue = $element->getAttribute('value');
- if ($currentValue === $value) {
- return;
- }
- }
-
- $xpath = '//select[@name="' . $name . '"]/ancestor::div[contains(@class, "ui") and contains(@class ,"dropdown")]';
- $xpath .= '| //div[@id="' . $name . '" and contains(@class, "ui") and contains(@class ,"dropdown") ]';
- try {
- $selectItem = self::$driver->findElement(WebDriverBy::xpath($xpath));
- $selectItem->click();
- $this->waitForAjax();
+ protected const INPUT_TYPES = [
+ 'text' => 'text',
+ 'password' => 'password',
+ 'number' => 'number',
+ 'hidden' => 'hidden',
+ 'search' => 'search'
+ ];
- // If search field exists input them before select
- $xpath = '//select[@name="' . $name . '"]/ancestor::div[contains(@class, "ui") and contains(@class ,"dropdown")]/input[contains(@class,"search")]';
- $xpath .= '| //div[@id="' . $name . '" and contains(@class, "ui") and contains(@class ,"dropdown") ]/input[contains(@class,"search")]';
- $inputItems = self::$driver->findElements(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- foreach ($inputItems as $inputItem) {
- $actions->moveToElement($inputItem);
- $actions->perform();
- $inputItem->click();
- $inputItem->clear();
- $inputItem->sendKeys($value);
+ /**
+ * Execute action with retry logic
+ */
+ protected function executeWithRetry(callable $action, int $maxAttempts = 3): mixed
+ {
+ $lastException = null;
+ for ($i = 0; $i < $maxAttempts; $i++) {
+ try {
+ return $action();
+ } catch (Exception $e) {
+ $lastException = $e;
+ $this->waitForAjax();
+ sleep(self::DEFAULT_DELAY);
}
-
- //Try to find need string with value
- $xpath = '//div[contains(@class, "menu") and contains(@class ,"visible")]/div[@data-value="' . $value . '"]';
- $menuItem = self::$driver->wait()->until(
- WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::xpath($xpath))
- );
- $menuItem->click();
- } catch (NoSuchElementException $e) {
- $this->fail('Not found select with name ' . $name . 'on selectDropdownItem' . PHP_EOL);
- } catch (TimeoutException $e) {
- $this->fail('Not found menuitem ' . $value . PHP_EOL);
- } catch (Exception $e) {
- $this->fail('Unknown error ' . $e->getMessage() . PHP_EOL);
}
+ throw $lastException ?? new RuntimeException('Action failed after retries');
}
/**
- * @param string $text
- * @param string $level
- * @return void
+ * Send command to BrowserStack
*/
- public static function annotate(string $text, string $level='info')
- {
- // Create an associative array with the structure you want to encode as JSON
+ protected function sendBrowserStackCommand(
+ BrowserStackAction $action,
+ array $arguments
+ ): void {
$data = [
- 'action' => 'annotate',
- 'arguments' => [
- 'level' => $level,
- 'data' => $text
- ]
+ 'action' => $action->value,
+ 'arguments' => $arguments
];
-
- // Encode the array as a JSON string
$message = 'browserstack_executor: ' . json_encode($data, JSON_PRETTY_PRINT);
+ self::$driver->executeScript($message);
+ }
- // Execute the script with the encoded message
- // Temporary disable because of many problems on BrowserStack self::$driver->executeScript($message);
+ /**
+ * Add annotation in BrowserStack
+ */
+ public static function annotate(string $text, string $level = 'info'): void
+ {
+ self::$driver->executeScript(
+ 'browserstack_executor: ' . json_encode([
+ 'action' => BrowserStackAction::ANNOTATE->value,
+ 'arguments' => [
+ 'level' => $level,
+ 'data' => $text
+ ]
+ ])
+ );
+ }
+
+ /**
+ * Set BrowserStack session status
+ */
+ public static function setSessionStatus(string $text, string $status = 'failed'): void
+ {
+ self::$driver->executeScript(
+ 'browserstack_executor: ' . json_encode([
+ 'action' => BrowserStackAction::SET_SESSION_STATUS->value,
+ 'arguments' => [
+ 'status' => $status,
+ 'reason' => substr($text, 0, 256)
+ ]
+ ])
+ );
}
+
+
+
+
/**
- * Wait until jquery will be ready
+ * Wait for AJAX requests to complete
*/
protected function waitForAjax(): void
{
self::annotate("Test action: Waiting for AJAX");
- while (true) // Handle timeout somewhere
- {
- $ajaxIsComplete = (bool)(self::$driver->executeScript("return window.jQuery&&jQuery.active == 0"));
- if ($ajaxIsComplete) {
- break;
- }
- sleep(1);
+ $this->executeWithRetry(function () {
+ return (bool)self::$driver->executeScript("return window.jQuery && jQuery.active == 0");
+ });
+ }
+
+ /**
+ * Find element safely without throwing exception
+ */
+ protected function findElementSafely(string $xpath): ?WebDriverElement
+ {
+ try {
+ return self::$driver->findElement(WebDriverBy::xpath($xpath));
+ } catch (NoSuchElementException) {
+ return null;
}
}
+ /**
+ * Wait for element presence
+ */
+ protected function waitForElement(string $xpath, int $timeout = self::WAIT_TIMEOUT): WebDriverElement
+ {
+ return self::$driver->wait($timeout, self::WAIT_INTERVAL)->until(
+ WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::xpath($xpath))
+ );
+ }
/**
- * Fails a test with the given message.
- *
- *
- * @psalm-return never-return
+ * Scroll element into view
*/
- public static function fail(string $message = ''): void
+ protected function scrollIntoView(WebDriverElement $element, string $block = 'center'): void
{
- self::setSessionStatus($message);
- parent::fail($message);
+ self::$driver->executeScript(
+ "arguments[0].scrollIntoView({block: '$block', behavior: '" . self::SCROLL_BEHAVIOR . "'})",
+ [$element]
+ );
}
/**
- * @param string $text
- * @param string $status failed/passed
- * @return void
+ * Handle action errors
*/
- public static function setSessionStatus(string $text, string $status = 'failed')
+ protected function handleActionError(string $action, string $message, Exception $e): void
{
+ $errorMessage = sprintf(
+ "Failed to %s: %s. Error: %s",
+ $action,
+ $message,
+ $e->getMessage()
+ );
+
+ $screenshotPath = $this->takeScreenshot($action);
+ self::annotate("Test failure: $errorMessage", 'error');
+ $this->fail("$errorMessage\nScreenshot saved at: $screenshotPath");
+ }
- // Create an associative array with the structure you want to encode as JSON
- $data = [
- 'action' => 'setSessionStatus',
- 'arguments' => [
- 'status' => $status,
- 'reason' => substr($text,0,256)
- ]
- ];
+ /**
+ * Take screenshot
+ */
+ protected function takeScreenshot(string $name): string
+ {
+ $screenshotDir = sys_get_temp_dir() . '/test-screenshots';
+ if (!is_dir($screenshotDir) && !mkdir($screenshotDir, 0777, true)) {
+ throw new RuntimeException("Failed to create screenshot directory");
+ }
- // Encode the array as a JSON string
- $message = 'browserstack_executor: ' . json_encode($data, JSON_PRETTY_PRINT);
+ $filename = sprintf(
+ '%s/%s_%s_%s.png',
+ $screenshotDir,
+ date('Y-m-d_H-i-s'),
+ $this->getName(),
+ $name
+ );
- // Execute the script with the encoded message
- // Temporary disable because of many problems on BrowserStack self::$driver->executeScript($message);
+ self::$driver->takeScreenshot($filename);
+ return $filename;
+ }
+
+ protected function logTestAction(string $action, array $context = []): void
+ {
+ $message = "Test action: $action";
+ if ($context) {
+ $message .= " Context: " . json_encode($context);
+ }
+ self::annotate($message);
+ }
+
+ /**
+ * Fails a test with the given message.
+ *
+ *
+ * @psalm-return never-return
+ */
+ public static function fail(string $message = ''): void
+ {
+ self::setSessionStatus($message);
+ parent::fail($message);
}
/**
@@ -169,7 +239,7 @@ public static function setSessionStatus(string $text, string $status = 'failed')
* @param string $name
* @return void
*/
- public static function setSessionName(string $name)
+ public static function setSessionName(string $name): void
{
// Create an associative array with the structure you want to encode as JSON
@@ -184,66 +254,84 @@ public static function setSessionName(string $name)
$message = 'browserstack_executor: ' . json_encode($data, JSON_PRETTY_PRINT);
// Execute the script with the encoded message
- // Temporary disable because of many problems on BrowserStack self::$driver->executeScript($message);
+ // Temporary disable because of many problems on BrowserStack
+ self::$driver->executeScript($message);
}
/**
* Select dropdown menu item
*
* @param $name string menu name identifier
- * @param $value string menu text for select
+ * @param $value string menu value for select
*
- * @return string
*/
- protected function selectDropdownItemByName(string $name, string $value): string
+ protected function selectDropdownItem(string $name, string $value): void
{
- self::annotate("Test action: Select $name menu item with text=$value");
- // Check selected value
- $xpath = '//select[@name="' . $name . '"]/option[@selected="selected"]';
- $selectedExtensions = self::$driver->findElements(WebDriverBy::xpath($xpath));
- foreach ($selectedExtensions as $element) {
- $currentValue = $element->getText();
- if ($currentValue === $value) {
- return $element->getAttribute('value');
- }
- }
+ $this->logTestAction("Select dropdown", ['name' => $name, 'value' => $value]);
- $xpath = '//select[@name="' . $name . '"]/ancestor::div[contains(@class, "ui") and contains(@class ,"dropdown")]';
- $xpath .= '| //div[@id="' . $name . '" and contains(@class, "ui") and contains(@class ,"dropdown") ]';
try {
- $selectItem = self::$driver->findElement(WebDriverBy::xpath($xpath));
- $selectItem->click();
- $this->waitForAjax();
+ $this->executeWithRetry(function () use ($name, $value) {
+ $dropdown = $this->findAndClickDropdown($name);
+ $this->waitForAjax();
+ $this->fillDropdownSearch($name, $value);
+ $this->selectDropdownValue($value);
+ });
+ } catch (Exception $e) {
+ $this->handleActionError('select dropdown item', "$name with value $value", $e);
+ }
+ }
- // If search field exists input them before select
- $xpath = '//select[@name="' . $name . '"]/ancestor::div[contains(@class, "ui") and contains(@class ,"dropdown")]/input[contains(@class,"search")]';
- $xpath .= '| //div[@id="' . $name . '" and contains(@class, "ui") and contains(@class ,"dropdown") ]/input[contains(@class,"search")]';
- $inputItems = self::$driver->findElements(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- foreach ($inputItems as $inputItem) {
- $actions->moveToElement($inputItem);
- $actions->perform();
- $inputItem->click();
- $inputItem->clear();
- $inputItem->sendKeys($value);
- }
+ /**
+ * Find and click dropdown
+ */
+ private function findAndClickDropdown(string $name): WebDriverElement
+ {
+ $xpath = sprintf(
+ '//select[@name="%s"]/ancestor::div[contains(@class, "ui") and contains(@class ,"dropdown")] | //div[@id="%s" and contains(@class, "ui") and contains(@class ,"dropdown")]',
+ $name,
+ $name
+ );
+ $dropdown = $this->waitForElement($xpath);
+ $this->scrollIntoView($dropdown);
+ $dropdown->click();
+ return $dropdown;
+ }
- //Try to find need string with value
- $xpath = '//div[contains(@class, "menu") and contains(@class ,"visible")]/div[contains(text(),"' . $value . '")]';
- $menuItem = self::$driver->wait()->until(
- WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::xpath($xpath))
- );
- $menuItem->click();
- return $menuItem->getAttribute('data-value');
- } catch (NoSuchElementException $e) {
- $this->fail('Not found select with name ' . $name . 'on selectDropdownItem' . PHP_EOL);
- } catch (TimeoutException $e) {
- $this->fail('Not found menuitem ' . $value . PHP_EOL);
- } catch (Exception $e) {
- $this->fail('Unknown error ' . $e->getMessage() . PHP_EOL);
+ /**
+ * Fill dropdown search field
+ */
+ private function fillDropdownSearch(string $name, string $value): void
+ {
+ $xpath = sprintf(
+ '//select[@name="%s"]/ancestor::div[contains(@class, "dropdown")]/input[contains(@class,"search")] | //div[@id="%s"]/input[contains(@class,"search")]',
+ $name,
+ $name
+ );
+
+ if ($searchInput = $this->findElementSafely($xpath)) {
+ $this->scrollIntoView($searchInput);
+ $searchInput->click();
+ $searchInput->clear();
+ $searchInput->sendKeys($value);
}
}
+ /**
+ * Select dropdown value
+ */
+ private function selectDropdownValue(string $value): void
+ {
+ $xpath = sprintf(
+ '//div[contains(@class, "menu") and contains(@class ,"visible")]/div[@data-value="%s"]',
+ $value
+ );
+ $menuItem = $this->waitForElement($xpath);
+ $menuItem->click();
+ }
+
+
+
+
/**
* Assert that menu item selected
*
@@ -251,17 +339,22 @@ protected function selectDropdownItemByName(string $name, string $value): string
* @param $checkedValue string checked value
* @param bool $skipIfNotExist
*/
- protected function assertMenuItemSelected(string $name, string $checkedValue, $skipIfNotExist = false): void
+ protected function assertMenuItemSelected(string $name, string $checkedValue, bool $skipIfNotExist = false): void
{
- $xpath = '//select[@name="' . $name . '"]/option[@selected="selected"]';
- $selectedExtensions = self::$driver->findElements(WebDriverBy::xpath($xpath));
- foreach ($selectedExtensions as $element) {
- $currentValue = $element->getAttribute('value');
- $message = "{$name} check failure, because {$checkedValue} != {$currentValue}";
- $this->assertEquals($checkedValue, $currentValue, $message);
+ $xpath = sprintf('//select[@name="%s"]/option[@selected="selected"]', $name);
+ $selected = $this->findElementSafely($xpath);
+
+ if (!$selected && !$skipIfNotExist) {
+ $this->fail("Menu item $name not found");
}
- if (!$skipIfNotExist && count($selectedExtensions) === 0) {
- $this->fail('Not found select with name ' . $name . ' in assertMenuItemSelected' . PHP_EOL);
+
+ if ($selected) {
+ $currentValue = $selected->getAttribute('value');
+ $this->assertEquals(
+ $checkedValue,
+ $currentValue,
+ "Menu item $name check failed: expected $checkedValue, got $currentValue"
+ );
}
}
@@ -272,9 +365,8 @@ protected function assertMenuItemSelected(string $name, string $checkedValue, $s
*/
protected function assertMenuItemNotSelected(string $name): void
{
- $xpath = '//select[@name="' . $name . '"]/option[@selected="selected"]';
+ $xpath = sprintf('//select[@name="%s"]/option[@selected="selected"]', $name);
$this->assertElementNotFound(WebDriverBy::xpath($xpath));
-
}
/**
@@ -284,12 +376,10 @@ protected function assertMenuItemNotSelected(string $name): void
*/
protected function assertElementNotFound($by): void
{
-
- $els = self::$driver->findElements($by);
- if (count($els)) {
- $this->fail("Unexpectedly element was found by " . $by->getValue() . PHP_EOL);
+ $elements = self::$driver->findElements($by);
+ if (count($elements) > 0) {
+ $this->fail("Unexpectedly element was found by " . $by->getValue());
}
- // increment assertion counter
$this->assertTrue(true);
}
@@ -302,19 +392,24 @@ protected function assertElementNotFound($by): void
*/
protected function changeTextAreaValue(string $name, string $value, bool $skipIfNotExist = false): void
{
- self::annotate("Test action: Change text area $name with value $value");
- $xpath = ('//textarea[@name="' . $name . '"]');
- $textAreaItems = self::$driver->findElements(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- foreach ($textAreaItems as $textAreaItem) {
- $actions->moveToElement($textAreaItem);
- $actions->perform();
- $textAreaItem->click();
- $textAreaItem->clear();
- $textAreaItem->sendKeys($value);
- }
- if (!$skipIfNotExist && count($textAreaItems) === 0) {
- $this->fail('Not found textarea with name ' . $name . ' in changeTextAreaValue' . PHP_EOL);
+ $this->logTestAction("Change textarea", ['name' => $name, 'value' => $value]);
+
+ try {
+ $xpath = sprintf('//textarea[@name="%s"]', $name);
+ $textArea = $this->findElementSafely($xpath);
+
+ if (!$textArea && !$skipIfNotExist) {
+ throw new RuntimeException("Textarea $name not found");
+ }
+
+ if ($textArea) {
+ $this->scrollIntoView($textArea);
+ $textArea->click();
+ $textArea->clear();
+ $textArea->sendKeys($value);
+ }
+ } catch (Exception $e) {
+ $this->handleActionError('change textarea value', $name, $e);
}
}
@@ -375,33 +470,28 @@ protected function assertInputFieldValueEqual(string $name, string $checkedValue
}
/**
- * Change checkbox state according the $enabled value if checkbox with the $name exist on the page
- *
- * @param string $name
- * @param bool $enabled
- * @param bool $skipIfNotExist
+ * Change checkbox state
*/
protected function changeCheckBoxState(string $name, bool $enabled, bool $skipIfNotExist = false): void
{
- self::annotate("Test action: Change checkbox $name to state $enabled");
- $xpath = '//input[@name="' . $name . '" and @type="checkbox"]';
- $checkBoxItems = self::$driver->findElements(WebDriverBy::xpath($xpath));
- foreach ($checkBoxItems as $checkBoxItem) {
- if (
- ($enabled && !$checkBoxItem->isSelected())
- ||
- (!$enabled && $checkBoxItem->isSelected())
- ) {
- $xpath = '//input[@name="' . $name . '" and @type="checkbox"]/parent::div';
- $checkBoxItem = self::$driver->findElement(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- $actions->moveToElement($checkBoxItem);
- $actions->perform();
- $checkBoxItem->click();
+ $this->logTestAction("Change checkbox", ['name' => $name, 'enabled' => $enabled]);
+
+ try {
+ $xpath = sprintf('//input[@name="%s" and @type="checkbox"]', $name);
+ $checkbox = $this->findElementSafely($xpath);
+
+ if (!$checkbox && !$skipIfNotExist) {
+ throw new RuntimeException("Checkbox $name not found");
}
- }
- if (!$skipIfNotExist && count($checkBoxItems) === 0) {
- $this->fail('Not found checkbox with name ' . $name . ' in changeCheckBoxState' . PHP_EOL);
+
+ if ($checkbox && $checkbox->isSelected() !== $enabled) {
+ $parentXpath = $xpath . '/parent::div';
+ $parentElement = self::$driver->findElement(WebDriverBy::xpath($parentXpath));
+ $this->scrollIntoView($parentElement);
+ $parentElement->click();
+ }
+ } catch (Exception $e) {
+ $this->handleActionError('change checkbox state', $name, $e);
}
}
@@ -429,36 +519,59 @@ protected function assertCheckBoxStageIsEqual(string $name, bool $enabled, bool
}
/**
- * Submit form with id - $formId and wait until form send
- *
- * @param string $formId
- *
+ * Submit form
*/
protected function submitForm(string $formId): void
{
- self::annotate("Test action: Submit the form");
- $xpath = '//form[@id="' . $formId . '"]//ancestor::div[@id="submitbutton"]';
+ $this->logTestAction("Submit form", ['id' => $formId]);
+
try {
- $button_Submit = self::$driver->findElement(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- $actions->moveToElement($button_Submit);
- $actions->perform();
- $button_Submit->click();
- $this->waitForAjax();
- self::$driver->wait(10, 500)->until(
- function ($driver) use ($xpath) {
- $button_Submit = $driver->findElement(WebDriverBy::xpath($xpath));
+ $this->executeWithRetry(function () use ($formId) {
+ $xpath = sprintf('//form[@id="%s"]//ancestor::div[@id="submitbutton"]', $formId);
+ $button = $this->waitForElement($xpath);
+ $this->scrollIntoView($button);
+ $button->click();
+ $this->waitForAjax();
- return $button_Submit->isEnabled();
- }
- );
- } catch (NoSuchElementException $e) {
- $this->fail('Not found submit button on this page' . PHP_EOL);
- } catch (TimeoutException $e) {
- $this->fail('Form doesn\'t send after 10 seconds timeout' . PHP_EOL);
+ // Wait for button to be enabled again
+ self::$driver->wait(self::WAIT_TIMEOUT, self::WAIT_INTERVAL)->until(
+ function () use ($xpath) {
+ return self::$driver->findElement(WebDriverBy::xpath($xpath))->isEnabled();
+ }
+ );
+ });
} catch (Exception $e) {
- $this->fail('Unknown error ' . $e->getMessage() . PHP_EOL);
+ $this->handleActionError('submit form', $formId, $e);
+ }
+ }
+
+ /**
+ * Fill form with data
+ */
+ protected function fillForm(array $data): void
+ {
+ $this->logTestAction("Fill form", $data);
+
+ foreach ($data as $name => $value) {
+ match (true) {
+ $this->isDropdown($name) => $this->selectDropdownItem($name, $value),
+ $this->isCheckbox($name) => $this->changeCheckBoxState($name, (bool)$value),
+ $this->isTextArea($name) => $this->changeTextAreaValue($name, (string)$value),
+ default => $this->changeInputField($name, (string)$value)
+ };
+ }
+ }
+
+ protected function getFormData(array $fields): array
+ {
+ $data = [];
+ foreach ($fields as $field) {
+ $element = $this->findElementSafely("//input[@name='$field'] | //textarea[@name='$field'] | //select[@name='$field']");
+ if ($element) {
+ $data[$field] = $element->getAttribute('value');
+ }
}
+ return $data;
}
/**
@@ -468,19 +581,20 @@ function ($driver) use ($xpath) {
*/
protected function clickSidebarMenuItemByHref(string $href): void
{
- self::annotate("Click sidebar menu item href=$href");
+ $this->logTestAction("Click sidebar menu", ['href' => $href]);
+
try {
- $xpath = '//div[@id="sidebar-menu"]//ancestor::a[contains(@class, "item") and contains(@href ,"' . $href . '")]';
- $sidebarItem = self::$driver->findElement(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- $actions->moveToElement($sidebarItem);
- $actions->perform();
- $sidebarItem->click();
+ $xpath = sprintf(
+ '//div[@id="sidebar-menu"]//ancestor::a[contains(@class, "item") and contains(@href ,"%s")]',
+ $href
+ );
+
+ $menuItem = $this->waitForElement($xpath);
+ $this->scrollIntoView($menuItem);
+ $menuItem->click();
$this->waitForAjax();
- } catch (NoSuchElementException $e) {
- $this->fail('Not found sidebar item with href=' . $href . ' on this page' . PHP_EOL);
} catch (Exception $e) {
- $this->fail('Unknown error ' . $e->getMessage() . PHP_EOL);
+ $this->handleActionError('click sidebar menu item', $href, $e);
}
}
@@ -561,19 +675,16 @@ protected function clickDeleteButtonOnRowWithText(string $text): void
*/
protected function clickButtonByHref(string $href): void
{
- self::annotate("Test action: Click button by href=$href");
+ $this->logTestAction("Click button", ['href' => $href]);
+
try {
- $xpath = "//a[@href = '{$href}']";
- $button_AddNew = self::$driver->findElement(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- $actions->moveToElement($button_AddNew);
- $actions->perform();
- $button_AddNew->click();
+ $xpath = sprintf('//a[@href="%s"]', $href);
+ $button = $this->waitForElement($xpath);
+ $this->scrollIntoView($button);
+ $button->click();
$this->waitForAjax();
- } catch (NoSuchElementException $e) {
- $this->fail('Not found button with href=' . $href . ' on this page on clickButtonByHref' . PHP_EOL);
} catch (Exception $e) {
- $this->fail('Unknown error ' . $e->getMessage() . PHP_EOL);
+ $this->handleActionError('click button', $href, $e);
}
}
@@ -584,22 +695,26 @@ protected function clickButtonByHref(string $href): void
*/
protected function changeTabOnCurrentPage(string $anchor): void
{
- self::annotate("Test action: Change tab with anchor=$anchor");
+ $this->logTestAction("Change tab", ['anchor' => $anchor]);
+
try {
- $jsScrollToTop = "document.getElementById('main').scrollIntoView({block: 'start', inline: 'nearest', behavior: 'instant'})";
- self::$driver->executeScript($jsScrollToTop);
- sleep(3); // Give a brief moment for the scroll action to complete
+ // Scroll to top first
+ self::$driver->executeScript(
+ "document.getElementById('main').scrollIntoView({block: 'start', inline: 'nearest', behavior: 'instant'})"
+ );
- $xpath = "//div[contains(@class, 'menu')]//a[contains(@data-tab,'{$anchor}')]";
- $tab = self::$driver->findElement(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- $actions->moveToElement($tab);
- $actions->perform();
+ sleep(self::DEFAULT_DELAY);
+
+ $xpath = sprintf(
+ '//div[contains(@class, "menu")]//a[contains(@data-tab,"%s")]',
+ $anchor
+ );
+
+ $tab = $this->waitForElement($xpath);
+ $this->scrollIntoView($tab);
$tab->click();
- } catch (NoSuchElementException $e) {
- $this->fail('Not found tab with anchor=' . $anchor . ' on this page in changeTabOnCurrentPage' . PHP_EOL);
} catch (Exception $e) {
- $this->fail('Unknown error ' . $e->getMessage() . PHP_EOL);
+ $this->handleActionError('change tab', $anchor, $e);
}
}
@@ -608,21 +723,19 @@ protected function changeTabOnCurrentPage(string $anchor): void
*/
protected function openAccordionOnThePage(): void
{
- self::annotate("Test action: Open accordion");
+ $this->logTestAction("Open accordion");
+
try {
- $xpath = "//div[contains(@class, 'ui') and contains(@class, 'accordion')]";
- $accordion = self::$driver->findElement(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- $actions->moveToElement($accordion);
- $actions->perform();
+ $xpath = '//div[contains(@class, "ui") and contains(@class, "accordion")]';
+ $accordion = $this->waitForElement($xpath);
+ $this->scrollIntoView($accordion);
$accordion->click();
- } catch (NoSuchElementException $e) {
- $this->fail('Not found usual accordion element on this page on openAccordionOnThePage' . PHP_EOL);
} catch (Exception $e) {
- $this->fail('Unknown error ' . $e->getMessage() . ' on openAccordionOnThePage' . PHP_EOL);
+ $this->handleActionError('open accordion', '', $e);
}
}
+
/**
* Get ID from hidden input at form
*
@@ -631,13 +744,12 @@ protected function openAccordionOnThePage(): void
protected function getCurrentRecordID(): string
{
try {
- $xpath = '//input[@name="id" and (@type="hidden")]';
- $input = self::$driver->findElement(WebDriverBy::xpath($xpath));
- return $input->getAttribute('value') ?? 'undefinedInGetCurrentRecordID';
- } catch (NoSuchElementException $e) {
- $this->fail('Not found input with name ID on this page getCurrentRecordID' . PHP_EOL);
+ $xpath = '//input[@name="id" and @type="hidden"]';
+ $input = $this->waitForElement($xpath);
+ return $input->getAttribute('value') ?? 'undefined';
} catch (Exception $e) {
- $this->fail('Unknown error ' . $e->getMessage() . PHP_EOL);
+ $this->handleActionError('get current record ID', '', $e);
+ return 'undefined';
}
}
@@ -650,20 +762,23 @@ protected function getCurrentRecordID(): string
*/
protected function deleteAllRecordsOnTable(string $tableId): void
{
- self::annotate("Test action: Delete all records on table with id=$tableId");
- $xpath = "//table[@id='{$tableId}']//a[contains(@href,'delete') and not(contains(@class,'disabled'))]";
- $deleteButtons = self::$driver->findElements(WebDriverBy::xpath($xpath));
- while (count($deleteButtons) > 0) {
- try {
- $deleteButton = self::$driver->findElement(WebDriverBy::xpath($xpath));
+ $this->logTestAction("Delete all records", ['tableId' => $tableId]);
+
+ try {
+ $xpath = sprintf(
+ '//table[@id="%s"]//a[contains(@href,"delete") and not(contains(@class,"disabled"))]',
+ $tableId
+ );
+
+ while ($deleteButton = $this->findElementSafely($xpath)) {
+ $this->scrollIntoView($deleteButton);
$deleteButton->click();
- sleep(1);
+ sleep(self::DEFAULT_DELAY);
$deleteButton->click();
$this->waitForAjax();
- unset($deleteButtons[0]);
- } catch (NoSuchElementException $e) {
- break;
}
+ } catch (Exception $e) {
+ $this->handleActionError('delete all records', $tableId, $e);
}
}
@@ -677,44 +792,23 @@ protected function deleteAllRecordsOnTable(string $tableId): void
*/
protected function checkIfElementExistOnDropdownMenu(string $name, string $value): bool
{
- self::annotate("Test action: Trying to click on element with text=$value on menu $name");
+ $this->logTestAction("Check dropdown element", ['name' => $name, 'value' => $value]);
- $xpath = '//select[@name="' . $name . '"]/ancestor::div[contains(@class, "ui") and contains(@class ,"dropdown")]';
- $xpath .= '| //div[@id="' . $name . '" and contains(@class, "ui") and contains(@class ,"dropdown") ]';
- $elementFound = false;
try {
- $selectItem = self::$driver->findElement(WebDriverBy::xpath($xpath));
- $selectItem->click();
+ $dropdown = $this->findAndClickDropdown($name);
$this->waitForAjax();
+ $this->fillDropdownSearch($name, $value);
- // If search field exists input them before select
- $xpath = '//select[@name="' . $name . '"]/ancestor::div[contains(@class, "ui") and contains(@class ,"dropdown")]/input[contains(@class,"search")]';
- $xpath .= '| //div[@id="' . $name . '" and contains(@class, "ui") and contains(@class ,"dropdown") ]/input[contains(@class,"search")]';
- $inputItems = self::$driver->findElements(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- foreach ($inputItems as $inputItem) {
- $actions->moveToElement($inputItem);
- $actions->perform();
- $inputItem->click();
- $inputItem->clear();
- $inputItem->sendKeys($value);
- }
-
- //Try to find need string with value
- $xpath = '//div[contains(@class, "menu") and contains(@class ,"visible")]/div[contains(text(),"' . $value . '")]';
- self::$driver->wait(5, 500)->until(
- WebDriverExpectedCondition::presenceOfElementLocated(
- WebDriverBy::xpath($xpath)
- )
+ $xpath = sprintf(
+ '//div[contains(@class, "menu") and contains(@class ,"visible")]/div[contains(text(),"%s")]',
+ $value
);
- $elementFound = true;
- self::annotate("Test action: Element with text=$value was found!");
- } catch (NoSuchElementException $e) {
- self::annotate("Test action: Element with text=$value not found (NoSuchElementException) " . $e->getMessage());
+
+ return $this->findElementSafely($xpath) !== null;
} catch (Exception $e) {
- self::annotate("Test action: Element with text=$value not found (Code Exception) " . $e->getMessage());
+ self::annotate("Element check failed: " . $e->getMessage(), 'warning');
+ return false;
}
- return $elementFound;
}
/**
@@ -727,21 +821,28 @@ protected function checkIfElementExistOnDropdownMenu(string $name, string $value
*/
protected function fillDataTableSearchInput(string $datatableId, string $name, string $value): void
{
+ $this->logTestAction("Fill datatable search", [
+ 'datatableId' => $datatableId,
+ 'name' => $name,
+ 'value' => $value
+ ]);
- self::annotate("Test action: Fill datatable search input field $name with value=$value");
-
- // Change the value of the input field
- $this->changeInputField($name, $value);
-
- // Use JavaScript to trigger a 'keyup' event with keyCode 13 (Enter) on the input element
- // This is needed to initiate the search in the DataTable
- self::$driver->executeScript("$('#{$name}').trigger($.Event('keyup', { keyCode: 13 }));");
+ try {
+ $this->changeInputField($name, $value);
- // Wait for any AJAX calls to complete
- $this->waitForAjax();
+ // Trigger search
+ self::$driver->executeScript(
+ sprintf(
+ "$('#%s').trigger($.Event('keyup', { keyCode: 13 }));",
+ $name
+ )
+ );
- // Wait for DataTableDraw
- usleep(5000000);
+ $this->waitForAjax();
+ usleep(5000000); // Wait for DataTable redraw
+ } catch (Exception $e) {
+ $this->handleActionError('fill datatable search', $name, $e);
+ }
}
/**
@@ -753,32 +854,56 @@ protected function fillDataTableSearchInput(string $datatableId, string $name, s
*/
protected function changeInputField(string $name, string $value, bool $skipIfNotExist = false): void
{
- self::annotate("Test action: Change input field $name with value $value");
+ $this->logTestAction("Change input field", ['name' => $name, 'value' => $value]);
- $xpath = '//input[@name="' . $name . '" and (@type="text" or @type="password" or @type="number" or @type="hidden" or @type="search")]';
- $inputItems = self::$driver->findElements(WebDriverBy::xpath($xpath));
- $actions = new WebDriverActions(self::$driver);
- foreach ($inputItems as $inputItem) {
- $id = $inputItem->getAttribute('id');
- $type = $inputItem->getAttribute('type');
- if ($type === 'hidden') {
- if (!empty($id)) {
- self::$driver->executeScript("document.getElementById('{$id}').value={$value}");
- }
- } else {
- $actions->moveToElement($inputItem);
- $actions->perform();
- if (!empty($id)) {
- self::$driver->executeScript("document.getElementById('{$id}').scrollIntoView({block: 'center'})");
+ try {
+ $types = implode(' or ', array_map(fn($type) => "@type='$type'", self::INPUT_TYPES));
+ $xpath = sprintf('//input[@name="%s" and (%s)]', $name, $types);
+
+ $input = $this->findElementSafely($xpath);
+ if (!$input && !$skipIfNotExist) {
+ throw new RuntimeException("Input field $name not found");
+ }
+
+ if ($input) {
+ $type = $input->getAttribute('type');
+ $id = $input->getAttribute('id');
+
+ if ($type === 'hidden' && $id) {
+ self::$driver->executeScript("document.getElementById('$id').value='$value'");
+ } else {
+ $this->scrollIntoView($input);
+ $input->click();
+ $input->clear();
+ $input->sendKeys($value);
}
- $inputItem->click();
- $inputItem->clear();
- $inputItem->sendKeys($value);
}
+ } catch (Exception $e) {
+ $this->handleActionError('change input field', $name, $e);
}
+ }
- if (!$skipIfNotExist && count($inputItems) === 0) {
- $this->fail('Not found input with name ' . $name . ' in changeInputField' . PHP_EOL);
- }
+ /**
+ * Helper method to check element types
+ */
+ private function isDropdown(string $name): bool
+ {
+ return (bool)$this->findElementSafely(
+ sprintf('//select[@name="%s"] | //div[@id="%s"][contains(@class,"dropdown")]', $name, $name)
+ );
+ }
+
+ private function isCheckbox(string $name): bool
+ {
+ return (bool)$this->findElementSafely(
+ sprintf('//input[@name="%s" and @type="checkbox"]', $name)
+ );
+ }
+
+ private function isTextArea(string $name): bool
+ {
+ return (bool)$this->findElementSafely(
+ sprintf('//textarea[@name="%s"]', $name)
+ );
}
-}
\ No newline at end of file
+}
diff --git a/tests/AdminCabinet/Lib/globals.php b/tests/AdminCabinet/Lib/globals.php
index e0445352c..063047408 100644
--- a/tests/AdminCabinet/Lib/globals.php
+++ b/tests/AdminCabinet/Lib/globals.php
@@ -1,4 +1,5 @@
clickSidebarMenuItemByHref('/admin-cabinet/call-queues/index/');
+ sleep(5);
+
// Routing
$this->clickSidebarMenuItemByHref('/admin-cabinet/incoming-routes/index/');
$this->clickButtonByHref('/admin-cabinet/incoming-routes/modify');
@@ -77,6 +81,10 @@ public function testDropdownsOnCreateDeleteQueue(array $params): void
$createCallQueue = new CreateCallQueueTest();
$createCallQueue->testCreateCallQueue($this->additionProvider()['Accountant department for test dropdown'][0]);
+ // Look at the current call queue content after creating
+ $this->clickSidebarMenuItemByHref('/admin-cabinet/call-queues/index/');
+ sleep(5);
+
// Routing
$this->clickSidebarMenuItemByHref('/admin-cabinet/incoming-routes/index/');
$this->clickButtonByHref('/admin-cabinet/incoming-routes/modify');
@@ -164,4 +172,4 @@ public function additionProvider(): array
]];
return $params;
}
-}
\ No newline at end of file
+}
diff --git a/tests/AdminCabinet/Tests/FillPBXSettingsTest.php b/tests/AdminCabinet/Tests/FillPBXSettingsTest.php
index 65dbd5d1e..cd6df2f62 100644
--- a/tests/AdminCabinet/Tests/FillPBXSettingsTest.php
+++ b/tests/AdminCabinet/Tests/FillPBXSettingsTest.php
@@ -1,4 +1,5 @@
'Тестовая 72',
- PbxSettings::PBX_DESCRIPTION => 'log: admin pass: 123456789MikoPBX#1 last test:' . date("Y-m-d H:i:s"),
+ PbxSettings::PBX_NAME => 'Тестовая 72',
+ PbxSettings::PBX_DESCRIPTION => 'log: admin pass: 123456789MikoPBX#1 last test:' . date("Y-m-d H:i:s"),
PbxSettings::PBX_LANGUAGE => 'en-en',
PbxSettings::PBX_RECORD_CALLS => true,
PbxSettings::SEND_METRICS => false,