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.

661 lines
15 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__).'/CElementQuery.php';
require_once dirname(__FILE__).'/CommandExecutor.php';
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\Exception\NoSuchAlertException;
use Facebook\WebDriver\WebDriverExpectedCondition;
/**
* Web page implementation.
*/
class CPage {
/**
* Page defaults.
*/
const DEFAULT_PAGE_WIDTH = 1440;
const DEFAULT_PAGE_HEIGHT = 1024;
/**
* Web driver instance.
*
* @var RemoteWebDriver
*/
protected $driver;
/**
* Local cookie cache.
*
* @var array
*/
protected static $cookie = null;
/**
* Page height.
*
* @var integer
*/
protected $height = null;
/**
* Page width.
*
* @var integer
*/
protected $width = null;
/**
* Viewport freeze flag.
*
* @var boolean
*/
protected $viewportUpdated = false;
/**
* Web driver and CElementQuery initialization.
*/
public function __construct() {
$this->connect();
CElementQuery::setPage($this);
}
/**
* Web driver initialization.
*/
public function connect() {
$capabilities = DesiredCapabilities::chrome();
if (defined('PHPUNIT_BROWSER_NAME')) {
$capabilities->setBrowserName(PHPUNIT_BROWSER_NAME);
}
if (!defined('PHPUNIT_BROWSER_NAME') || PHPUNIT_BROWSER_NAME === 'chrome') {
$options = new ChromeOptions();
$options->addArguments([
'--no-sandbox',
'--enable-font-antialiasing=false',
'--window-size='.self::DEFAULT_PAGE_WIDTH.','.self::DEFAULT_PAGE_HEIGHT,
'--disable-dev-shm-usage'
]);
$capabilities->setCapability(ChromeOptions::CAPABILITY, $options);
}
$phpunit_driver_address = PHPUNIT_DRIVER_ADDRESS;
if (strpos($phpunit_driver_address, ':') === false) {
$phpunit_driver_address .= ':4444';
}
$this->driver = RemoteWebDriver::create('http://'.$phpunit_driver_address.'/wd/hub', $capabilities);
$this->driver->setCommandExecutor(new CommandExecutor($this->driver->getCommandExecutor()));
$this->driver->manage()->window()->setSize(
new WebDriverDimension(self::DEFAULT_PAGE_WIDTH, self::DEFAULT_PAGE_HEIGHT)
);
}
/**
* Perform page cleanup.
* Close all popup windows, switch to the initial window, remove cookies.
*/
public function cleanup() {
$this->resetViewport();
if (self::$cookie !== null) {
foreach ($this->driver->manage()->getCookies() as $cookie) {
if ($cookie->getName() === 'zbx_session') {
if ($cookie->getValue() !== self::$cookie['value']) {
self::$cookie = null;
}
break;
}
}
}
$this->driver->manage()->deleteAllCookies();
try {
$this->driver->executeScript('sessionStorage.clear();');
} catch (Exception $exception) {
// Code is not missing here.
}
$windows = $this->driver->getWindowHandles();
if (count($windows) <= 1) {
return true;
}
try {
foreach (array_slice($windows, 1) as $window) {
$this->driver->switchTo()->window($window);
$this->driver->close();
}
}
catch (Exception $exception) {
// Error handling is not missing here.
}
if (count($this->driver->getWindowHandles()) >= 1) {
try {
$this->driver->switchTo()->window($windows[0]);
}
catch (Exception $exception) {
return false;
}
}
return true;
}
/**
* Destroy web page.
*/
public function destroy() {
$this->driver->quit();
self::$cookie = null;
}
/**
* Reconnect web driver.
*/
public function reset() {
$this->destroy();
$this->connect();
}
/**
* Login as specified user.
*
* @param string $sessionid
* @param integer $userid
*
* @return $this
*/
public function login(string $sessionid = '09e7d4286dfdca4ba7be15e0f3b2b55a', $userid = 1) {
$session = CDBHelper::getRow('SELECT status FROM sessions WHERE sessionid='.zbx_dbstr($sessionid));
if (!$session) {
$secret = bin2hex(random_bytes(16));
DBexecute('INSERT INTO sessions (sessionid,userid,secret)'.
' VALUES ('.zbx_dbstr($sessionid).','.$userid.','.zbx_dbstr($secret).')'
);
}
elseif ($session['status'] != 0) { /* ZBX_SESSION_ACTIVE */
DBexecute('UPDATE sessions SET status=0 WHERE sessionid='.zbx_dbstr($sessionid));
}
if (self::$cookie !== null) {
$cookie = json_decode(base64_decode(urldecode(self::$cookie['value'])), true);
}
if (self::$cookie === null || $sessionid !== $cookie['sessionid']) {
$data = ['sessionid' => $sessionid];
$config = CDBHelper::getRow('SELECT session_key FROM config WHERE configid=1');
$data['sign'] = hash_hmac('sha256', json_encode($data), $config['session_key'], false);
$path = parse_url(PHPUNIT_URL, PHP_URL_PATH);
self::$cookie = [
'name' => 'zbx_session',
'value' => base64_encode(json_encode($data)),
'path' => rtrim(substr($path, 0, strrpos($path, '/')), '/')
];
$this->driver->get(PHPUNIT_URL);
}
$this->driver->manage()->addCookie(self::$cookie);
return $this;
}
/**
* Logout and clean cookies.
*/
public function logout() {
try {
// Before logout open page without any scripts, otherwise session might be restored and logout won't work.
$this->open('setup.php');
$session = null;
if (self::$cookie === null) {
foreach ($this->driver->manage()->getCookies() as $cookie) {
if ($cookie->getName() === 'zbx_session') {
$session = $cookie->getValue();
break;
}
}
}
else {
$session = self::$cookie['value'];
}
if ($session !== null) {
DBExecute('DELETE FROM sessions WHERE sessionid='.zbx_dbstr($session));
}
$this->driver->manage()->deleteAllCookies();
self::$cookie = null;
}
catch (\Exception $e) {
throw new \Exception('Cannot logout user: '.$e->getTraceAsString());
}
}
/**
* Open specified URL.
*
* @param string $url URL to be opened.
*
* @return $this
*/
public function open($url) {
$this->driver->get(PHPUNIT_URL.$url);
return $this;
}
/**
* Get page title.
*
* @return string
*/
public function getTitle() {
return $this->driver->getTitle();
}
/**
* Get current page URL.
*
* @return string
*/
public function getCurrentUrl() {
return $this->driver->getCurrentURL();
}
/**
* Set width and height of viewport.
*
* @param int $width
* @param int $height
*/
protected function setViewport($width, $height) {
try {
CommandExecutor::executeCustom($this->driver, [
'cmd' => 'Emulation.setDeviceMetricsOverride',
'params' => [
'width' => $width,
'height' => $height,
'deviceScaleFactor' => 1,
'mobile' => false,
'fitWindow' => false
]
]);
} catch (Exception $exception) {
// Code is not missing here.
}
}
/**
* Setting "frozen" viewport size.
*/
public function updateViewport() {
try {
if (!$this->driver->executeScript('return !!window.chrome;')) {
throw new Exception();
}
} catch (Exception $exception) {
return false;
}
try {
// Calculate page width and height depending on sidemenu and scrollbars presence.
$size = $this->driver->executeScript(
'var side = document.getElementsByClassName("sidebar")[0];'.
'var wrapper = document.getElementsByClassName("wrapper")[0];'.
'var width = ((typeof side !== "undefined") ? side.scrollWidth : 0)'.
'+ ((typeof wrapper !== "undefined") ? wrapper.scrollWidth : 0);'.
'var height = Math.max((typeof wrapper !== "undefined") ? wrapper.scrollHeight : 0,'.
'(typeof side !== "undefined") ? side.scrollHeight : 0);'.
'return'.
'[(width !== 0) ? width : window.getComputedStyle(document.documentElement)["width"],'.
'(height !== 0) ? height : window.getComputedStyle(document.documentElement)["height"],'.
'(typeof wrapper !== "undefined" && wrapper.scrollWidth >'.
'parseInt(window.getComputedStyle(wrapper)["width"], 10)) ? 20 : 0];'
);
$this->width = (int)$size[0];
// Screenshot is 1px smaller to ensure that scroll is still present.
$this->height = (int)$size[1] - 1;
if ($this->height > self::DEFAULT_PAGE_HEIGHT || $this->width > self::DEFAULT_PAGE_WIDTH) {
$this->setViewport(max([
// Add 20px to page width when vertical scroll presents.
$this->width + (int)$size[2], self::DEFAULT_PAGE_WIDTH]),
max([$this->height, self::DEFAULT_PAGE_HEIGHT
]));
$this->viewportUpdated = true;
}
} catch (Exception $exception) {
// Code is not missing here.
}
return true;
}
/**
* Resetting viewport size to default.
*/
public function resetViewport() {
if ($this->viewportUpdated === false) {
return;
}
if (isset($this->height) && $this->height > self::DEFAULT_PAGE_HEIGHT) {
try {
CommandExecutor::executeCustom($this->driver, [
'cmd' => 'Emulation.clearDeviceMetricsOverride',
'params' => ['clear' => true]
]);
} catch (Exception $exception) {
// Code is not missing here.
}
$this->height = self::DEFAULT_PAGE_HEIGHT;
}
$this->viewportUpdated = false;
}
/**
* Take screenshot of current page.
*
* @return string
*/
protected function takePageScreenshot() {
if ($this->viewportUpdated === true || !$this->updateViewport()) {
return $this->driver->takeScreenshot();
}
$screenshot = $this->driver->takeScreenshot();
$this->resetViewport();
return $screenshot;
}
/**
* Take screenshot of current page or page element.
*
* @param CElement|null $element page element to get screenshot of
*
* @return string
*/
public function takeScreenshot($element = null) {
$screenshot = $this->takePageScreenshot();
if ($element !== null) {
$screenshot = CImageHelper::getImageRegion($screenshot, $element->getRect());
}
return $screenshot;
}
/**
* Get browser logs.
*
* @return array
*/
public function getBrowserLog() {
return $this->driver->manage()->getLog('browser');
}
/**
* Get page source.
*
* @return type
*/
public function getSource() {
return $this->driver->getPageSource();
}
/**
* Wait until page is ready.
*
* @param integer $timeout timeout in seconds
*/
public function waitUntilReady($timeout = null) {
return (new CElementQuery(null))->waitUntilReady($timeout);
}
/**
* Check if alert is present.
*
* @return boolean
*/
public function isAlertPresent() {
return ($this->getAlertText() !== null);
}
/**
* Get alert text.
*
* @return string|null
*/
public function getAlertText() {
try {
return $this->driver->switchTo()->alert()->getText();
}
catch (NoSuchAlertException $exception) {
return null;
}
}
/**
* Wait until alert is present and accept it.
*/
public function acceptAlert() {
CElementQuery::wait()->until(WebDriverExpectedCondition::alertIsPresent());
$this->driver->switchTo()->alert()->accept();
}
/**
* Wait until alert is present and dismiss it.
*/
public function dismissAlert() {
CElementQuery::wait()->until(WebDriverExpectedCondition::alertIsPresent());
$this->driver->switchTo()->alert()->dismiss();
}
/**
* Emulate key presses.
*
* @param array|string $keys keys to be pressed
*/
public function pressKey($keys) {
if (!is_array($keys)) {
$keys = [$keys];
}
$keyboard = $this->driver->getKeyboard();
foreach ($keys as $key) {
$keyboard->pressKey($key);
}
}
/**
* Create CElementQuery instance.
* @see CElementQuery
*
* @param string $type selector type (method) or selector
* @param string $locator locator part of selector
*
* @return CElementQuery
*/
public function query($type, $locator = null) {
return new CElementQuery($type, $locator);
}
/**
* Get web driver instance.
*
* @return RemoteWebDriver
*/
public function getDriver() {
return $this->driver;
}
/**
* Remove focus from the element.
*/
public function removeFocus() {
try {
$this->driver->executeScript('for (var i = 0; i < 5; i++) if (document.activeElement.tagName !== "BODY")'.
' document.activeElement.blur(); else break;');
}
catch (\Exception $ex) {
throw new \Exception('Cannot remove focus.');
}
}
/**
* Refresh page.
*
* @return $this
*/
public function refresh() {
$this->driver->navigate()->refresh();
return $this;
}
/**
* Switching to frame or iframe.
*
* @param CElement|string|array|null $element iframe element
*
* @return $this
*/
public function switchTo($element = null) {
if ($element === null) {
$this->driver->switchTo()->defaultContent();
return $this;
}
if (is_string($element)) {
$element = $this->query($element)->one(false);
}
elseif (is_array($element)) {
$element = $this->query($element[0], $element[1])->one(false);
}
if ($element instanceof RemoteWebElement) {
$this->driver->switchTo()->frame($element);
}
else {
throw new \Exception('Cannot switch to frame that is not an element.');
}
return $this;
}
/**
* Allows to login with user credentials.
*
* @param string $alias Username on login screen
* @param string $password Password on login screen
* @param string $url Direct link to certain Zabbix page
*/
public function userLogin($alias, $password, $url = 'index.php') {
if (self::$cookie === null) {
$this->driver->get(PHPUNIT_URL);
}
$this->logout();
$this->open($url);
$this->query('id:name')->waitUntilVisible()->one()->fill($alias);
$this->query('id:password')->one()->fill($password);
$this->query('id:enter')->one()->click();
$this->waitUntilReady();
// Make sure that logged in page is opened.
try {
$this->query('xpath://aside[@class="sidebar"]//a[text()="User settings"]')->exists();
}
catch (\Exception $ex) {
throw new \Exception('"User settings" menu is not found on page. Probably user is not logged in.');
}
}
/**
* Check page title text.
*
* @param string $title page title
*
* @throws Exception
*/
public function assertTitle($title) {
global $ZBX_SERVER_NAME;
if ($ZBX_SERVER_NAME !== '') {
$title = $ZBX_SERVER_NAME.NAME_DELIMITER.$title;
}
$text = $this->getTitle();
if ($text !== $title) {
throw new \Exception('Title of the page "'.$text.'" is not equal to "'.$title.'".');
}
}
/**
* Check page header.
*
* @param string $header page header to be compared
*
* @throws Exception
*/
public function assertHeader($header) {
$text = $this->query('xpath://h1[@id="page-title-general"]')->one()->getText();
if ($text !== $header) {
throw new \Exception('Header of the page "'.$text.'" is not equal to "'.$header.'".');
}
}
/**
* Scroll page to the top position.
*/
public function scrollToTop() {
$this->getDriver()->executeScript('document.getElementsByClassName(\'wrapper\')[0].scrollTo(0, 0)');
}
}