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.

801 lines
17 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__).'/CBaseElement.php';
require_once dirname(__FILE__).'/CElementQuery.php';
require_once dirname(__FILE__).'/IWaitable.php';
require_once dirname(__FILE__).'/WaitableTrait.php';
require_once dirname(__FILE__).'/CastableTrait.php';
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\WebDriverKeys;
/**
* Generic web page element.
*/
class CElement extends CBaseElement implements IWaitable {
/**
* Element can be used as waitable object to wait for element state changes.
*/
use WaitableTrait;
/**
* Element can be used as castable object to cast element to specific type.
*/
use CastableTrait;
/**
* Element selector.
*
* @var WebDriverBy
*/
protected $by;
/**
* Parent element (if any).
* Parent element is never set for elements retrieved from element collection.
*
* @var CElement
*/
protected $parent;
/**
* Flag that allows to disable normalizing.
*
* @var boolean
*/
protected $normalized = false;
/**
* Initialize element.
*
* @param RemoteWebElement $element
* @param type $options
*/
public static function createInstance(RemoteWebElement $element, $options = []) {
$instance = new static($element->executor, $element->id, $element->isW3cCompliant);
$instance->setElement($element);
foreach ($options as $key => $value) {
$instance->$key = $value;
}
if (!$instance->normalized) {
$instance->normalize();
}
return $instance;
}
/**
* Simplified selector for elements that can be located directly on page.
* @throws Exception
*/
public static function find() {
throw new Exception('Element cannot be located without selector.');
}
/**
* Invalidate element state.
* This method should be overridden in order to reset cached objects that will be broken during reload operation.
* @see CBaseElement::reload
*/
public function invalidate() {
// Code is not missing here.
}
/**
* Reload stalled element if reload is possible.
*
* @return $this
*
* @throws Exception
*/
public function reload() {
if ($this->by === null) {
throw new Exception('Cannot reload stalled element selected as a part of multi-element selection.');
}
if ($this->parent !== null && $this->parent->isStalled()) {
$this->parent->reload();
}
$this->invalidate();
$query = new CElementQuery($this->by);
if ($this->parent !== null) {
$query->setContext($this->parent);
}
$this->setElement($query->waitUntilPresent()->one());
if (!$this->normalized) {
$this->normalize();
}
return $this;
}
/**
* Set new base element.
*
* @param RemoteWebElement $element element to be set
*/
protected function setElement(RemoteWebElement $element) {
$this->executor = $element->executor;
$this->id = $element->id;
$this->fileDetector = $element->fileDetector;
$this->isW3cCompliant = $element->isW3cCompliant;
}
/**
* Perform element selector normalization.
* This method can be overridden to check if element is selected properly.
*/
protected function normalize() {
// Code is not missing here.
}
/**
* Get parent selection query.
*
* @param mixed $type selector type (method) or selector
* @param string $locator locator part of selector
*
* @return CElementQuery
*/
public function parents($type = null, $locator = null) {
$selector = 'xpath:./ancestor';
if ($type !== null) {
$selector .= '::'.CXPathHelper::fromSelector($type, $locator);
}
else {
$selector .= '::*';
}
return $this->query($selector)->setReversedOrder();
}
/**
* Get children selection query.
*
* @param mixed $type selector type (method) or selector
* @param string $locator locator part of selector
*
* @return CElementQuery
*/
public function children($type = null, $locator = null) {
$selector = 'xpath:./*';
if ($type !== null) {
$selector = 'xpath:./'.CXPathHelper::fromSelector($type, $locator);
}
return $this->query($selector);
}
/**
* Get element query with current element context.
*
* @param mixed $type selector type (method) or selector
* @param string $locator locator part of selector
*
* @return CElementQuery
*/
public function query($type, $locator = null) {
$query = new CElementQuery($type, $locator);
$query->setContext($this);
return $query;
}
/**
* Dispatch HTML event to an element.
*
* @param string $event event type
*
* @return $this
*/
public function fireEvent($event = 'change') {
$driver = CElementQuery::getDriver();
$driver->executeScript('arguments[0].dispatchEvent(new Event(arguments[1]));', [$this, $event]);
return $this;
}
/**
* Highlight element by setting orange border around it.
* This method should be used for test debugging purposes only.
*
* @return $this
*/
public function highlight() {
$driver = CElementQuery::getDriver();
$driver->executeScript('arguments[0].style.border="3px solid #ff9800";', [$this]);
return $this;
}
/**
* Take screenshot of the specific element.
*
* @return string
*/
public function takeScreenshot() {
return CElementQuery::getPage()->takeScreenshot($this);
}
/**
* Get instance of specified element class from current element.
*
* @param string $class class to be casted to
* @param array $options additional options passed to object
*
* @return CElement
*/
public function cast($class, $options = []) {
return call_user_func([$class, 'createInstance'], $this, array_merge($options, [
'parent' => $this->parent,
'by' => $this->by
]));
}
/**
* @inheritdoc
*/
public function getText() {
if (!$this->isVisible()) {
return CElementQuery::getDriver()->executeScript('return arguments[0].textContent;', [$this]);
}
return parent::getText();
}
/**
* Get element value.
*
* @return type
*/
public function getValue() {
return CElementQuery::getDriver()->executeScript('return arguments[0].value;', [$this]);
}
/**
* @inheritdoc
*/
public function sendKeys($value) {
if (is_string($value) && strpos($value, '"') === false) {
parent::sendKeys($value);
}
else {
CElementQuery::getDriver()->executeScript('arguments[0].value=arguments[1];arguments[0].focus();',
[$this, $value]
);
}
return $this;
}
/**
* Highlight the value in the field.
*
* @return $this
*/
public function selectValue() {
CElementQuery::getDriver()->executeScript('arguments[0].focus();arguments[0].select();', [$this]);
return $this;
}
/**
* Overwrite value in field.
*
* @param $text text to be written into the field
*
* @return $this
*/
public function overwrite($text) {
if ($text === '' || $text === null) {
$text = WebDriverKeys::DELETE;
}
return $this->selectValue()->type($text);
}
/**
* Alias for overwrite.
* @see self::overwrite
*
* @param $text text to be written into the field
*
* @return $this
*/
public function fill($text) {
if (!is_array($text) && preg_match('/[\x{10000}-\x{10FFFF}]/u', $text) === 1) {
CElementQuery::getDriver()->executeScript('arguments[0].value = '.json_encode($text).';', [$this]);
}
else {
return $this->overwrite($text);
}
}
/**
* Remove element from the page.
*/
public function delete() {
CElementQuery::getDriver()->executeScript('arguments[0].remove();', [$this]);
}
/**
* Get element rectangle.
*
* @return array
*/
public function getRect() {
$location = $this->getLocation();
$size = $this->getSize();
return [
'x' => $location->getX(),
'y' => $location->getY(),
'width' => $size->getWidth(),
'height' => $size->getHeight()
];
}
/**
* Hover over an element.
*
* @return $this
*/
public function hover() {
return $this->fireEvent('mouseover');
}
/**
* Check if element is clickable.
*
* @return boolean
*/
public function isClickable() {
return $this->isDisplayed() && $this->isEnabled();
}
/**
* @inheritdoc
*/
public function getClickableCondition() {
$target = $this;
return function () use ($target) {
return $target->isClickable();
};
}
/**
* Check if element is present.
*
* @return boolean
*/
public function isPresent() {
return !$this->isStalled();
}
/**
* @inheritdoc
*/
public function getPresentCondition() {
$target = $this;
return function () use ($target) {
return $target->isPresent();
};
}
/**
* @inheritdoc
*/
public function getVisibleCondition() {
$target = $this;
return function () use ($target) {
return $target->isVisible();
};
}
/**
* @inheritdoc
*/
public function getSelectedCondition() {
$target = $this;
return function () use ($target) {
return $target->isSelected();
};
}
/**
* Check if text is present.
*
* @param string $text text to be present
*
* @return boolean
*/
public function isTextPresent($text) {
return (strpos($this->getText(), $text) !== false);
}
/**
* @inheritdoc
*/
public function getTextPresentCondition($text) {
$target = $this;
return function () use ($target, $text) {
return $target->isTextPresent($text);
};
}
/**
* Check presence of the attribute(s).
*
* @param string|array $attributes attribute or attributes to be present.
*
* @return boolean
*/
public function isAttributePresent($attributes) {
if (!is_array($attributes)) {
$attributes = [$attributes];
}
foreach ($attributes as $key => $value) {
if (is_numeric($key)) {
if ($this->getAttribute($value) === null) {
return false;
}
}
elseif ($this->getAttribute($key) !== $value) {
return false;
}
}
return true;
}
/**
* @inheritdoc
*/
public function getAttributesPresentCondition($attributes) {
$target = $this;
return function () use ($target, $attributes) {
return $target->isAttributePresent($attributes);
};
}
/**
* @inheritdoc
*/
public function getClassesPresentCondition($classes) {
$target = $this;
return function () use ($target, $classes) {
return $target->hasClass($classes);
};
}
/**
* Check if element is ready.
*
* @return boolean
*/
public function isReady() {
return call_user_func($this->getReadyCondition());
}
/**
* @inheritdoc
*/
public function isEnabled($enabled = true) {
$attribute = parent::getAttribute('class');
$classes = ($attribute !== null) ? explode(' ', $attribute) : [];
$is_enabled = parent::isEnabled()
&& (parent::getAttribute('disabled') === null)
&& (!array_intersect(['disabled', 'readonly'], $classes))
&& (parent::getAttribute('readonly') === null);
return $is_enabled === $enabled;
}
/**
* @inheritdoc
*/
public function click($force = false) {
try {
return parent::click();
} catch (Exception $exception) {
if (!$force) {
throw $exception;
}
$this->forceClick();
}
return $this;
}
/**
* Force click on element.
*
* @return $this
*/
public function forceClick() {
try {
CElementQuery::getDriver()->executeScript('arguments[0].click();', [$this]);
}
catch (StaleElementReferenceException $exception) {
if (!$this->reload_staled) {
throw $exception;
}
$this->reload();
CElementQuery::getDriver()->executeScript('arguments[0].click();', [$this]);
}
return $this;
}
/**
* @inheritdoc
*/
public function getReadyCondition() {
return $this->getClickableCondition();
}
/**
* Wait until element changes it's state from stalled to normal.
*
* @param integer $timeout timeout in seconds
*
* @return $this
* @throws Exception
*/
public function waitUntilReloaded($timeout = null) {
if ($this->by === null) {
throw new Exception('Cannot wait for element reload on element selected in multi-element query.');
}
$element = $this;
$wait = forward_static_call_array([CElementQuery::class, 'wait'], $timeout !== null ? [$timeout] : []);
$wait->until(function () use ($element) {
if ($element->isStalled()) {
$element->reload();
return !$element->isStalled();
}
return null;
}
);
return $this;
}
/**
* Wait until element is selected.
*
* @param integer $timeout timeout in seconds
*
* @return $this
*/
public function waitUntilSelected($timeout = null) {
$wait = forward_static_call_array([CElementQuery::class, 'wait'], $timeout !== null ? [$timeout] : []);
$wait->until(WebDriverExpectedCondition::elementToBeSelected($this));
return $this;
}
/**
* Detect element by its tag or class.
*
* @param type $options
*/
public function detect($options = []) {
$tag = $this->getTagName();
if ($tag === 'textarea') {
return $this->asElement($options);
}
if ($tag === 'select') {
return $this->asList($options);
}
if ($tag === 'z-select') {
return $this->asDropdown($options);
}
if ($tag === 'table') {
return $this->asTable($options);
}
if ($tag === 'input') {
$type = $this->getAttribute('type');
if ($type === 'checkbox' || $type === 'radio') {
return $this->asCheckbox($options);
}
else {
return $this->asElement($options);
}
}
$class = explode(' ', $this->getAttribute('class'));
if (in_array('multiselect-control', $class)) {
return $this->asMultiselect($options);
}
if (in_array('radio-list-control', $class)) {
return $this->asSegmentedRadio($options);
}
if (in_array('checkbox-list', $class)) {
return $this->asCheckboxList($options);
}
if (in_array('range-control', $class) || in_array('calendar-control', $class)) {
return $this->asCompositeInput($options);
}
if (in_array('color-picker', $class)) {
return $this->asColorPicker($options);
}
if (in_array('multilineinput-control', $class)) {
return $this->asMultiline($options);
}
if (in_array('macro-input-group', $class)) {
return $this->asInputGroup($options);
}
CTest::zbxAddWarning('No specific element was detected');
return $this;
}
/**
* Throw error for not supported method invocation.
*
* @param string $method method name
*
* @throws Exception
*/
public static function onNotSupportedMethod($method) {
throw new Exception('Method "'.$method.'" is not supported by "'.static::class.'" class elements.');
}
/**
* Check element value.
*
* @param mixed $expected expected value of the element
*
* @return boolean
*
* @throws Exception
*/
public function checkValue($expected, $raise_exception = true) {
$value = $this->getValue();
if ($value === null) {
if ($raise_exception) {
throw new Exception('Cannot get value of the non-interactable element.');
}
return false;
}
if (is_array($value)) {
if (!is_array($expected)) {
$expected = [$expected];
}
foreach (['value', 'expected'] as $var) {
$values = [];
foreach ($$var as $item) {
$values[] = '"'.$item.'"';
}
sort($values);
$$var = implode(', ', $values);
}
}
if ($expected != $value && $raise_exception) {
if (!is_scalar($value) || is_bool($value)) {
$value = json_encode($value);
}
if (!is_scalar($expected) || is_bool($expected)) {
$expected = json_encode($expected);
}
throw new Exception('Element value "'.$value.'" doesn\'t match expected "'.$expected.'".');
}
return ($expected == $value);
}
/**
* Remove focus from the element.
*
* @return $this
*/
public function removeFocus() {
CElementQuery::getDriver()->executeScript('arguments[0].blur();', [$this]);
return $this;
}
/**
* Scroll the element to the top position.
*/
public function scrollToTop() {
CElementQuery::getDriver()->executeScript('arguments[0].scrollTo(0, 0)', [$this]);
}
/**
* Scroll the element to the visible position.
*/
public function scrollIntoView() {
CElementQuery::getDriver()->executeScript('arguments[0].scrollIntoView({behavior:\'instant\',block:\'end\',inline:\'nearest\'});', [$this]);
}
/**
* Check presence of the class(es).
*
* @param string|array $class class or classes to be present.
*
* @return boolean
*/
public function hasClass($class) {
$attribute = parent::getAttribute('class');
$classes = ($attribute !== null) ? explode(' ', $attribute) : [];
if (!is_array($class)) {
$class = [$class];
}
return (count(array_diff($class, $classes)) === 0);
}
/**
* Hover mouse over the element
*/
public function hoverMouse() {
$mouse = CElementQuery::getDriver()->getMouse();
$mouse->mouseMove($this->getCoordinates());
return $this;
}
}