You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
646 lines
16 KiB
646 lines
16 KiB
<?php
|
|
/*
|
|
** Zabbix
|
|
** Copyright (C) 2001-2023 Zabbix SIA
|
|
**
|
|
** This program is free software; you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation; either version 2 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program; if not, write to the Free Software
|
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**/
|
|
|
|
require_once 'vendor/autoload.php';
|
|
|
|
require_once dirname(__FILE__).'/CElement.php';
|
|
require_once dirname(__FILE__).'/CElementCollection.php';
|
|
require_once dirname(__FILE__).'/CElementFilter.php';
|
|
require_once dirname(__FILE__).'/elements/CNullElement.php';
|
|
require_once dirname(__FILE__).'/elements/CFormElement.php';
|
|
require_once dirname(__FILE__).'/elements/CGridFormElement.php';
|
|
require_once dirname(__FILE__).'/elements/CCheckboxFormElement.php';
|
|
require_once dirname(__FILE__).'/elements/CTableElement.php';
|
|
require_once dirname(__FILE__).'/elements/CTableRowElement.php';
|
|
require_once dirname(__FILE__).'/elements/CWidgetElement.php';
|
|
require_once dirname(__FILE__).'/elements/CDashboardElement.php';
|
|
require_once dirname(__FILE__).'/elements/CListElement.php';
|
|
require_once dirname(__FILE__).'/elements/CDropdownElement.php';
|
|
require_once dirname(__FILE__).'/elements/CCheckboxElement.php';
|
|
require_once dirname(__FILE__).'/elements/COverlayDialogElement.php';
|
|
require_once dirname(__FILE__).'/elements/CMainMenuElement.php';
|
|
require_once dirname(__FILE__).'/elements/CMessageElement.php';
|
|
require_once dirname(__FILE__).'/elements/CMultiselectElement.php';
|
|
require_once dirname(__FILE__).'/elements/CSegmentedRadioElement.php';
|
|
require_once dirname(__FILE__).'/elements/CCheckboxListElement.php';
|
|
require_once dirname(__FILE__).'/elements/CMultifieldTableElement.php';
|
|
require_once dirname(__FILE__).'/elements/CMultilineElement.php';
|
|
require_once dirname(__FILE__).'/elements/CColorPickerElement.php';
|
|
require_once dirname(__FILE__).'/elements/CCompositeInputElement.php';
|
|
require_once dirname(__FILE__).'/elements/CPopupMenuElement.php';
|
|
require_once dirname(__FILE__).'/elements/CPopupButtonElement.php';
|
|
require_once dirname(__FILE__).'/elements/CInputGroupElement.php';
|
|
require_once dirname(__FILE__).'/elements/CHostInterfaceElement.php';
|
|
require_once dirname(__FILE__).'/elements/CFilterElement.php';
|
|
require_once dirname(__FILE__).'/elements/CFieldsetElement.php';
|
|
|
|
require_once dirname(__FILE__).'/IWaitable.php';
|
|
require_once dirname(__FILE__).'/WaitableTrait.php';
|
|
require_once dirname(__FILE__).'/CastableTrait.php';
|
|
|
|
use Facebook\WebDriver\WebDriverBy;
|
|
use Facebook\WebDriver\Exception\NoSuchElementException;
|
|
use Facebook\WebDriver\Exception\WebDriverException;
|
|
|
|
/**
|
|
* Element selection query.
|
|
*/
|
|
class CElementQuery implements IWaitable {
|
|
|
|
/**
|
|
* Query can be used as waitable object to wait for elements before retrieving them.
|
|
*/
|
|
use WaitableTrait;
|
|
|
|
/**
|
|
* Query can be used as castable object to cast elements to specific type when elements are retrieved.
|
|
*/
|
|
use CastableTrait;
|
|
|
|
/**
|
|
* Wait iteration step duration.
|
|
*/
|
|
const WAIT_ITERATION = 50;
|
|
|
|
/**
|
|
* Timeout in seconds.
|
|
*/
|
|
const WAIT_TIMEOUT = 20;
|
|
|
|
/**
|
|
* Element selector.
|
|
*
|
|
* @var WebDriverBy
|
|
*/
|
|
protected $by;
|
|
|
|
/**
|
|
* Selector context (can be set to specific element or to global level).
|
|
*
|
|
* @var mixed
|
|
*/
|
|
protected $context;
|
|
|
|
/**
|
|
* Class to be used to instantiate elements.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $class;
|
|
|
|
/**
|
|
* Options applied to instantiated elements.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $options = [];
|
|
|
|
/**
|
|
* Element reverse order flag.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $reverse_order = false;
|
|
|
|
/**
|
|
* Shared web page instance.
|
|
*
|
|
* @var CPage
|
|
*/
|
|
protected static $page;
|
|
|
|
/**
|
|
* Last input element selector.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $selector;
|
|
|
|
/**
|
|
* Initialize element query by specified selector.
|
|
*
|
|
* @param mixed $type selector type (method) or selector
|
|
* @param string $locator locator part of selector
|
|
*/
|
|
public function __construct($type, $locator = null) {
|
|
$this->class = 'CElement';
|
|
$this->context = static::getDriver();
|
|
|
|
if ($type !== null) {
|
|
$this->by = static::getSelector($type, $locator);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get selector from type and locator.
|
|
*
|
|
* @param mixed $type selector type (method) or selector
|
|
* @param string $locator locator part of selector
|
|
*
|
|
* @return WebDriverBy
|
|
*/
|
|
public static function getSelector($type, $locator = null) {
|
|
if ($type instanceof WebDriverBy) {
|
|
return $type;
|
|
}
|
|
|
|
if ($locator === null) {
|
|
if (!is_array($type)) {
|
|
if ($type !== 'button') {
|
|
$parts = explode(':', $type, 2);
|
|
if (count($parts) !== 2) {
|
|
throw new Exception('Element selector "'.$type.'" is not well formatted.');
|
|
}
|
|
|
|
list($type, $locator) = $parts;
|
|
}
|
|
}
|
|
else {
|
|
$selectors = [];
|
|
foreach ($type as $selector) {
|
|
$selectors[] = './/'.CXPathHelper::fromSelector($selector);
|
|
}
|
|
|
|
$type = 'xpath';
|
|
$locator = implode('|', $selectors);
|
|
}
|
|
}
|
|
else if (is_array($locator)) {
|
|
foreach ($locator as $selector) {
|
|
$selectors[] = './/'.CXPathHelper::fromSelector($type, $selector);
|
|
}
|
|
|
|
$type = 'xpath';
|
|
$locator = implode('|', $selectors);
|
|
}
|
|
|
|
$mapping = [
|
|
'css' => 'cssSelector',
|
|
'class' => 'className',
|
|
'tag' => 'tagName',
|
|
'link' => 'linkText',
|
|
'button' => function () use ($locator) {
|
|
if ($locator === null) {
|
|
return WebDriverBy::tagName('button');
|
|
}
|
|
|
|
return WebDriverBy::xpath('.//button[normalize-space(text())='.CXPathHelper::escapeQuotes($locator).']');
|
|
}
|
|
];
|
|
|
|
if (array_key_exists($type, $mapping)) {
|
|
if (is_callable($mapping[$type])) {
|
|
return call_user_func($mapping[$type]);
|
|
}
|
|
else {
|
|
$type = $mapping[$type];
|
|
}
|
|
}
|
|
|
|
return call_user_func([WebDriverBy::class, $type], $locator);
|
|
}
|
|
|
|
/**
|
|
* Set query context.
|
|
*
|
|
* @param CElement $context context to be set
|
|
*/
|
|
public function setContext($context) {
|
|
$this->context = $context;
|
|
}
|
|
|
|
/**
|
|
* Get query context.
|
|
*
|
|
* @return CElement
|
|
*/
|
|
public function getContext() {
|
|
return $this->context;
|
|
}
|
|
|
|
/**
|
|
* Get last selector.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public static function getLastSelector() {
|
|
return static::$selector;
|
|
}
|
|
|
|
/**
|
|
* Set web page instance.
|
|
*
|
|
* @param CPage $page web page instance to be set
|
|
*/
|
|
public static function setPage($page) {
|
|
self::$page = $page;
|
|
}
|
|
|
|
/**
|
|
* Get web driver instance.
|
|
*
|
|
* @return RemoteWebDriver
|
|
*/
|
|
public static function getDriver() {
|
|
if (self::$page === null) {
|
|
return null;
|
|
}
|
|
|
|
return self::$page->getDriver();
|
|
}
|
|
|
|
/**
|
|
* Set reversed element order flag.
|
|
*
|
|
* @param boolean $order order to set
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setReversedOrder($order = true) {
|
|
$this->reverse_order = $order;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get web page instance.
|
|
*
|
|
* @return CPage
|
|
*/
|
|
public static function getPage() {
|
|
return self::$page;
|
|
}
|
|
|
|
/**
|
|
* Apply chained element query.
|
|
*
|
|
* @param mixed $type selector type (method) or selector
|
|
* @param string $locator locator part of selector
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function query($type, $locator = null) {
|
|
$prefix = ($this->by->getMechanism() !== 'xpath')
|
|
? './/'.CXPathHelper::fromWebDriverBy($this->by)
|
|
: $this->by->getValue();
|
|
|
|
if ($this->reverse_order) {
|
|
$prefix .= '[1]';
|
|
$this->reverse_order = false;
|
|
}
|
|
|
|
$by = self::getSelector($type, $locator);
|
|
$suffix = ($by->getMechanism() !== 'xpath')
|
|
? '//'.CXPathHelper::fromWebDriverBy($by)
|
|
: $by->getValue();
|
|
|
|
if (substr($suffix, 0, 1) !== '/') {
|
|
$suffix = '/'.$suffix;
|
|
}
|
|
|
|
$this->by = static::getSelector('xpath', $prefix.$suffix);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get wait instance.
|
|
*
|
|
* @return WebDriverWait
|
|
*/
|
|
public static function wait($timeout = null, $iteration = null) {
|
|
if ($iteration === null) {
|
|
$iteration = self::WAIT_ITERATION;
|
|
}
|
|
if ($timeout === null) {
|
|
$timeout = self::WAIT_TIMEOUT;
|
|
}
|
|
|
|
return static::getDriver()->wait($timeout, $iteration);
|
|
}
|
|
|
|
/**
|
|
* Wait until condition is met for target.
|
|
*
|
|
* @param IWaitable $target target for wait operation
|
|
* @param string $condition condition to be waited for
|
|
* @param array $params condition params
|
|
* @param integer $timeout timeout in seconds
|
|
*/
|
|
public static function waitUntil($target, $condition, $params = [], $timeout = null) {
|
|
$selector = $target->getSelectorAsText();
|
|
if ($selector !== null) {
|
|
$selector = ' located by '.$selector;
|
|
}
|
|
|
|
$callable = call_user_func_array([$target, CElementFilter::getConditionCallable($condition)], $params);
|
|
self::wait($timeout)->until($callable, 'Failed to wait for element'.$selector.' to be '.$condition.'.');
|
|
}
|
|
|
|
/**
|
|
* Get one element located by specified query.
|
|
*
|
|
* @param boolean $should_exist if method is allowed to return null as a result
|
|
*
|
|
* @return CElement
|
|
*/
|
|
public function one($should_exist = true) {
|
|
$class = $this->class;
|
|
$parent = ($this->context !== static::getDriver()) ? $this->context : null;
|
|
|
|
for ($i = 0; $i < 2; $i++) {
|
|
try {
|
|
if (!$this->reverse_order) {
|
|
$element = $this->context->findElement($this->by);
|
|
}
|
|
else {
|
|
$elements = $this->context->findElements($this->by);
|
|
if (!$elements) {
|
|
throw new NoSuchElementException('');
|
|
}
|
|
|
|
$element = end($elements);
|
|
}
|
|
|
|
break;
|
|
}
|
|
catch (NoSuchElementException $exception) {
|
|
if (!$should_exist) {
|
|
return new CNullElement(array_merge($this->options, ['parent' => $parent, 'by' => $this->by]));
|
|
}
|
|
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
return call_user_func([$class, 'createInstance'], $element, array_merge($this->options, [
|
|
'parent' => $parent,
|
|
'by' => $this->by
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* Get all elements located by specified query.
|
|
*
|
|
* @return CElement
|
|
*/
|
|
public function all() {
|
|
$class = $this->class;
|
|
|
|
$elements = $this->context->findElements($this->by);
|
|
|
|
if ($this->reverse_order) {
|
|
$elements = array_reverse($elements);
|
|
}
|
|
|
|
if ($this->class !== 'RemoteWebElement') {
|
|
foreach ($elements as &$element) {
|
|
$element = call_user_func([$class, 'createInstance'], $element, $this->options);
|
|
}
|
|
unset($element);
|
|
}
|
|
|
|
return new CElementCollection($elements, $class);
|
|
}
|
|
|
|
/**
|
|
* Get count of elements located by specified query.
|
|
*
|
|
* @return integer
|
|
*/
|
|
public function count() {
|
|
return $this->all()->count();
|
|
}
|
|
|
|
/**
|
|
* Set element class and options.
|
|
*
|
|
* @param string $class class to be used to instantiate elements
|
|
* @param array $options additional options passed to object
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function cast($class, $options = []) {
|
|
$this->class = $class;
|
|
$this->options = $options;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function getClickableCondition() {
|
|
$target = $this;
|
|
|
|
return function () use ($target) {
|
|
return $target->one(false)->isClickable();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function getReadyCondition() {
|
|
$driver = static::getDriver();
|
|
|
|
return function () use ($driver) {
|
|
return $driver->executeScript('return document.readyState === \'complete\' && (window.jQuery||{active:0}).active === 0;');
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function getPresentCondition() {
|
|
$target = $this;
|
|
|
|
return function () use ($target) {
|
|
return $target->one(false)->isValid();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function getTextPresentCondition($text) {
|
|
$target = $this;
|
|
|
|
return function () use ($target, $text) {
|
|
$element = $target->one(false);
|
|
if (!$element->isValid()) {
|
|
return false;
|
|
}
|
|
|
|
return (strpos($element->getText(), $text) !== false);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function getAttributesPresentCondition($attributes) {
|
|
$target = $this;
|
|
|
|
return function () use ($target, $attributes) {
|
|
$element = $target->one(false);
|
|
if (!$element->isValid()) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($attributes as $key => $value) {
|
|
if (is_numeric($key) && $element->getAttribute($value) === null) {
|
|
return false;
|
|
}
|
|
elseif ($element->getAttribute($key) !== $value) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function getClassesPresentCondition($classes) {
|
|
$target = $this;
|
|
|
|
return function () use ($target, $classes) {
|
|
return $target->one(false)->hasClass($classes);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function getVisibleCondition() {
|
|
$target = $this;
|
|
|
|
return function () use ($target) {
|
|
return $target->one(false)->isVisible();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function getSelectedCondition() {
|
|
$target = $this;
|
|
|
|
return function () use ($target) {
|
|
return $target->one(false)->isSelected();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check that the corresponding element exists.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function exists() {
|
|
return $this->one(false)->isValid();
|
|
}
|
|
|
|
/**
|
|
* Get input element from container.
|
|
*
|
|
* @param CElement $target container element
|
|
* @param string $prefix xpath prefix
|
|
* @param array|string $class element classes to look for
|
|
*
|
|
* @return CElement|CNullElement
|
|
*/
|
|
public static function getInputElement($target, $prefix = './', $class = null) {
|
|
$classes = [
|
|
'CElement' => [
|
|
// TODO: change after DEV-1630 (1) is resolved.
|
|
'/input[@name][not(@type) or @type="text" or @type="password"][not(@style) or not(contains(@style,"display: none"))]',
|
|
'/textarea[@name]'
|
|
],
|
|
'CListElement' => '/select[@name]',
|
|
'CDropdownElement' => '/z-select[@name]',
|
|
'CCheckboxElement' => '/input[@name][@type="checkbox" or @type="radio"]',
|
|
'CMultiselectElement' => [
|
|
'/div[contains(@class, "multiselect-control")]',
|
|
'/div/div[contains(@class, "multiselect-control")]' // TODO: remove after fix DEV-2510.
|
|
],
|
|
'CSegmentedRadioElement' => [
|
|
'/ul[contains(@class, "radio-list-control")]',
|
|
'/ul/li/ul[contains(@class, "radio-list-control")]',
|
|
'/div/ul[contains(@class, "radio-list-control")]' // TODO: remove after fix DEV-2510 and DEV-2511.
|
|
],
|
|
'CCheckboxListElement' => [
|
|
'/ul[contains(@class, "checkbox-list")]',
|
|
'/ul[contains(@class, "list-check-radio")]'
|
|
],
|
|
'CHostInterfaceElement' => [
|
|
'/div/div[contains(@class, "interface-container")]/../..'
|
|
],
|
|
'CMultifieldTableElement' => [
|
|
'/table',
|
|
'/div/table', // TODO: remove after fix DEV-2510.
|
|
'/*[contains(@class, "table-forms-separator")]/table'
|
|
],
|
|
'CCompositeInputElement' => [
|
|
'/div[contains(@class, "range-control")]',
|
|
'/div[contains(@class, "calendar-control")]'
|
|
],
|
|
'CColorPickerElement' => '/div[contains(@class, "color-picker")]',
|
|
'CMultilineElement' => '/div[contains(@class, "multilineinput-control")]',
|
|
'CInputGroupElement' => '/div[contains(@class, "macro-input-group")]',
|
|
'CFieldsetElement' => '/fieldset'
|
|
];
|
|
|
|
if ($class !== null) {
|
|
if (!is_array($class)) {
|
|
$class = [$class];
|
|
}
|
|
|
|
foreach (array_keys($classes) as $name) {
|
|
if (!in_array($name, $class)) {
|
|
unset($classes[$name]);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($classes as $class => $selectors) {
|
|
if (!is_array($selectors)) {
|
|
$selectors = [$selectors];
|
|
}
|
|
|
|
$xpaths = [];
|
|
foreach ($selectors as $selector) {
|
|
$xpaths[] = $prefix.$selector;
|
|
}
|
|
|
|
static::$selector = 'xpath:'.implode('|', $xpaths);
|
|
$element = $target->query(static::$selector)->cast($class)->one(false);
|
|
if ($element->isValid()) {
|
|
return $element;
|
|
}
|
|
}
|
|
|
|
static::$selector = null;
|
|
return new CNullElement(['locator' => 'input element']);
|
|
}
|
|
}
|