Translated using Weblate (Portuguese)
[phpmyadmin.git] / tests / end-to-end / TestBase.php
blobfc5fd99b108bc1f11b617e99b1cb41e0a40bd1cf
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin\Tests\Selenium;
7 use Closure;
8 use Exception;
9 use Facebook\WebDriver\Chrome\ChromeOptions;
10 use Facebook\WebDriver\Exception\InvalidSelectorException;
11 use Facebook\WebDriver\Exception\NoSuchElementException;
12 use Facebook\WebDriver\Exception\WebDriverException;
13 use Facebook\WebDriver\Remote\DesiredCapabilities;
14 use Facebook\WebDriver\Remote\RemoteWebDriver;
15 use Facebook\WebDriver\Remote\RemoteWebElement;
16 use Facebook\WebDriver\WebDriverBy;
17 use Facebook\WebDriver\WebDriverElement;
18 use Facebook\WebDriver\WebDriverExpectedCondition;
19 use Facebook\WebDriver\WebDriverSelect;
20 use InvalidArgumentException;
21 use PHPUnit\Framework\SkippedTest;
22 use PHPUnit\Framework\TestCase;
23 use Throwable;
25 use function bin2hex;
26 use function curl_errno;
27 use function curl_error;
28 use function curl_exec;
29 use function curl_init;
30 use function curl_setopt;
31 use function current;
32 use function end;
33 use function file_put_contents;
34 use function getenv;
35 use function is_bool;
36 use function is_object;
37 use function is_string;
38 use function json_decode;
39 use function json_encode;
40 use function mb_strtolower;
41 use function preg_match;
42 use function property_exists;
43 use function random_bytes;
44 use function reset;
45 use function sprintf;
46 use function str_ends_with;
47 use function strlen;
48 use function substr;
49 use function time;
50 use function trim;
51 use function usleep;
53 use const CURLOPT_CUSTOMREQUEST;
54 use const CURLOPT_HTTPHEADER;
55 use const CURLOPT_POSTFIELDS;
56 use const CURLOPT_RETURNTRANSFER;
57 use const CURLOPT_URL;
58 use const CURLOPT_USERPWD;
59 use const DIRECTORY_SEPARATOR;
60 use const JSON_PRETTY_PRINT;
61 use const JSON_UNESCAPED_SLASHES;
63 abstract class TestBase extends TestCase
65 protected RemoteWebDriver $webDriver;
67 /**
68 * Name of database for the test
70 public string $databaseName;
72 /**
73 * The session Id (Browserstack)
75 protected string $sessionId;
77 /**
78 * The window handle for the SQL tab
80 private string|null $sqlWindowHandle = null;
82 private const SESSION_REST_URL = 'https://api.browserstack.com/automate/sessions/';
84 /**
85 * Create a test database for this test class
87 protected static bool $createDatabase = true;
89 /**
90 * Did the test create the phpMyAdmin storage database ?
92 private bool $hadStorageDatabaseInstall = false;
94 /**
95 * Configures the selenium and database link.
97 * @throws Exception
99 protected function setUp(): void
102 * Needs to be implemented
104 * @ENV TESTSUITE_SELENIUM_COVERAGE
105 * @ENV TESTSUITE_FULL
107 parent::setUp();
109 if ($this->getHubUrl() === '') {
110 self::markTestSkipped('Selenium testing is not configured.');
113 if ($this->getTestSuiteUrl() === '') {
114 self::markTestSkipped('The ENV "TESTSUITE_URL" is not defined.');
117 if ($this->getTestSuiteUserLogin() === '') {
118 //TODO: handle config mode
119 self::markTestSkipped(
120 'The ENV "TESTSUITE_USER" is not defined, you may also want to define "TESTSUITE_PASSWORD".',
124 $capabilities = $this->getCapabilities();
125 $this->addCapabilities($capabilities);
126 $url = $this->getHubUrl();
128 $this->webDriver = RemoteWebDriver::create($url, $capabilities);
130 // The session Id is only used by BrowserStack
131 if ($this->hasBrowserstackConfig()) {
132 $this->sessionId = $this->webDriver->getSessionID();
135 $this->navigateTo('');
136 $this->webDriver->manage()->window()->maximize();
138 if (! static::$createDatabase) {
139 // Stop here, we were not asked to create a database
140 return;
143 $this->createDatabase();
147 * Create a test database
149 protected function createDatabase(): void
151 $this->databaseName = $this->getDbPrefix() . bin2hex(random_bytes(4));
152 $this->dbQuery(
153 'CREATE DATABASE IF NOT EXISTS `' . $this->databaseName . '`; USE `' . $this->databaseName . '`;',
155 static::$createDatabase = true;
158 public function getDbPrefix(): string
160 $envVar = getenv('TESTSUITE_DATABASE_PREFIX');
161 if ($envVar) {
162 return $envVar;
165 return '';
168 private function getBrowserStackCredentials(): string
170 return (string) getenv('TESTSUITE_BROWSERSTACK_USER') . ':' . (string) getenv('TESTSUITE_BROWSERSTACK_KEY');
173 protected function getTestSuiteUserLogin(): string
175 $user = getenv('TESTSUITE_USER');
177 return $user === false ? '' : $user;
180 protected function getTestSuiteUserPassword(): string
182 $user = getenv('TESTSUITE_PASSWORD');
184 return $user === false ? '' : $user;
187 protected function getTestSuiteUrl(): string
189 $user = getenv('TESTSUITE_URL');
191 return $user === false ? '' : $user;
195 * Has CI config ( CI_MODE == selenium )
197 public function hasCIConfig(): bool
199 $mode = getenv('CI_MODE');
200 if (empty($mode)) {
201 return false;
204 return $mode === 'selenium';
208 * Has ENV variables set for Browserstack
210 public function hasBrowserstackConfig(): bool
212 return ! empty(getenv('TESTSUITE_BROWSERSTACK_USER'))
213 && ! empty(getenv('TESTSUITE_BROWSERSTACK_KEY'));
217 * Has ENV variables set for local Selenium server
219 public function hasSeleniumConfig(): bool
221 return ! empty(getenv('TESTSUITE_SELENIUM_HOST'))
222 && ! empty(getenv('TESTSUITE_SELENIUM_PORT'));
226 * Get the selenium hub url
228 private function getHubUrl(): string
230 if ($this->hasBrowserstackConfig()) {
231 return 'https://'
232 . $this->getBrowserStackCredentials() .
233 '@hub-cloud.browserstack.com/wd/hub';
236 if ($this->hasSeleniumConfig()) {
237 return 'http://'
238 . (string) getenv('TESTSUITE_SELENIUM_HOST') . ':'
239 . (string) getenv('TESTSUITE_SELENIUM_PORT') . '/wd/hub';
242 return '';
246 * Navigate to URL
248 * @param string $url The URL
250 private function navigateTo(string $url): void
252 $suiteUrl = getenv('TESTSUITE_URL');
253 if ($suiteUrl === false) {
254 $suiteUrl = '';
257 if (str_ends_with($suiteUrl, '/')) {
258 $url = $suiteUrl . $url;
259 } else {
260 $url = $suiteUrl . '/' . $url;
263 $this->webDriver->get($url);
267 * Get the current running test name
269 * Usefull for browserstack
271 * @see https://github.com/phpmyadmin/phpmyadmin/pull/14595#issuecomment-418541475
272 * Reports the name of the test to browserstack
274 public function getTestName(): string
276 $className = substr(static::class, strlen('PhpMyAdmin\Tests\Selenium\\'));
278 return $className . ': ' . $this->name();
282 * Add specific capabilities
284 * @param DesiredCapabilities $capabilities The capabilities object
286 public function addCapabilities(DesiredCapabilities $capabilities): void
288 $buildLocal = true;
289 $buildId = 'Manual';
290 $projectName = 'phpMyAdmin';
291 $buildTagEnv = getenv('BUILD_TAG');
292 $githubActionEnv = getenv('GITHUB_ACTION');
294 if ($buildTagEnv) {
295 $buildId = $buildTagEnv;
296 $buildLocal = false;
297 $projectName = 'phpMyAdmin (Jenkins)';
298 } elseif ($githubActionEnv) {
299 $buildId = 'github-' . $githubActionEnv;
300 $buildLocal = true;
301 $projectName = 'phpMyAdmin (GitHub - Actions)';
304 if (! $buildLocal) {
305 return;
308 $capabilities->setCapability(
309 'bstack:options',
311 'os' => 'Windows',
312 'osVersion' => '10',
313 'resolution' => '1920x1080',
314 'projectName' => $projectName,
315 'sessionName' => $this->getTestName(),
316 'buildName' => $buildId,
317 'localIdentifier' => $buildId,
318 'local' => $buildLocal,
319 'debug' => false,
320 'consoleLogs' => 'verbose',
321 'networkLogs' => true,
327 * Get basic capabilities
329 public function getCapabilities(): DesiredCapabilities
331 switch (getenv('TESTSUITE_SELENIUM_BROWSER')) {
332 case 'chrome':
333 default:
334 $capabilities = DesiredCapabilities::chrome();
335 $chromeOptions = new ChromeOptions();
336 $chromeOptions->addArguments(['--lang=en']);
337 $capabilities->setCapability(ChromeOptions::CAPABILITY_W3C, $chromeOptions);
338 $capabilities->setCapability(
339 'loggingPrefs',
340 ['browser' => 'ALL'],
343 if ($this->hasCIConfig() && $this->hasBrowserstackConfig()) {
344 $capabilities->setCapability(
345 'os',
346 'Windows', // Force windows
348 $capabilities->setCapability(
349 'os_version',
350 '10', // Force windows 10
352 $capabilities->setCapability(
353 'browser_version',
354 '80.0', // Force chrome 80.0
356 $capabilities->setCapability('resolution', '1920x1080');
359 return $capabilities;
361 case 'safari':
362 $capabilities = DesiredCapabilities::safari();
363 if ($this->hasCIConfig() && $this->hasBrowserstackConfig()) {
364 $capabilities->setCapability(
365 'os',
366 'OS X', // Force OS X
368 $capabilities->setCapability(
369 'os_version',
370 'Sierra', // Force OS X Sierra
372 $capabilities->setCapability(
373 'browser_version',
374 '10.1', // Force Safari 10.1
378 return $capabilities;
380 case 'edge':
381 $capabilities = DesiredCapabilities::microsoftEdge();
382 if ($this->hasCIConfig() && $this->hasBrowserstackConfig()) {
383 $capabilities->setCapability(
384 'os',
385 'Windows', // Force windows
387 $capabilities->setCapability(
388 'os_version',
389 '10', // Force windows 10
391 $capabilities->setCapability(
392 'browser_version',
393 'insider preview', // Force Edge insider preview
397 return $capabilities;
402 * Checks whether the user is a superuser.
404 protected function isSuperUser(): bool
406 return $this->dbQuery('SELECT COUNT(*) FROM mysql.user');
410 * Skips test if test user is not a superuser.
412 protected function skipIfNotSuperUser(): void
414 if ($this->isSuperUser()) {
415 return;
418 self::markTestSkipped('Test user is not a superuser.');
422 * Use the fix relation button to install phpMyAdmin storage
424 protected function fixUpPhpMyAdminStorage(): bool
426 $this->navigateTo('index.php?route=/check-relations');
428 $fixTextSelector = '//div[@class="alert alert-primary" and contains(., "Create a database named")]/a';
429 if ($this->isElementPresent('xpath', $fixTextSelector)) {
430 $this->byXPath($fixTextSelector)->click();
431 $this->waitAjax();
433 return true;
436 return false;
440 * Skips test if pmadb is not configured.
442 protected function skipIfNotPMADB(): void
444 $this->navigateTo('index.php?route=/check-relations');
445 $pageContent = $this->waitForElement('id', 'page_content');
446 if (preg_match('/Configuration of pmadb… not OK/i', $pageContent->getText()) !== 1) {
447 return;
450 if (! $this->fixUpPhpMyAdminStorage()) {
451 self::markTestSkipped('The phpMyAdmin configuration storage is not working.');
454 // If it failed the code already has exited with markTestSkipped
455 $this->hadStorageDatabaseInstall = true;
459 * perform a login
461 * @param string $username Username
462 * @param string $password Password
464 public function login(string $username = '', string $password = ''): void
466 $this->logOutIfLoggedIn();
467 if ($username === '') {
468 $username = $this->getTestSuiteUserLogin();
471 if ($password === '') {
472 $password = $this->getTestSuiteUserPassword();
475 $this->navigateTo('');
476 /* Wait while page */
477 while ($this->webDriver->executeScript('return document.readyState !== "complete";')) {
478 usleep(5000);
481 // Return if already logged in
482 if ($this->isSuccessLogin()) {
483 return;
486 // Select English if the Language selector is available
487 if ($this->isElementPresent('id', 'languageSelect')) {
488 $this->selectByLabel($this->byId('languageSelect'), 'English');
491 // Clear the input for Microsoft Edge (remembers the username)
492 $this->waitForElement('id', 'input_username')->clear()->click()->sendKeys($username);
493 $this->byId('input_password')->click()->sendKeys($password);
494 $this->byId('input_go')->click();
498 * Get element by Id
500 * @param string $id The element ID
502 public function byId(string $id): RemoteWebElement
504 return $this->webDriver->findElement(WebDriverBy::id($id));
508 * Get element by css selector
510 * @param string $selector The element css selector
512 public function byCssSelector(string $selector): RemoteWebElement
514 return $this->webDriver->findElement(WebDriverBy::cssSelector($selector));
518 * Get element by xpath
520 * @param string $xpath The xpath
522 public function byXPath(string $xpath): RemoteWebElement
524 return $this->webDriver->findElement(WebDriverBy::xpath($xpath));
528 * Get element by linkText
530 * @param string $linkText The link text
532 public function byLinkText(string $linkText): RemoteWebElement
534 return $this->webDriver->findElement(WebDriverBy::linkText($linkText));
538 * Double click
540 public function doubleclick(): void
542 $this->webDriver->action()->doubleClick()->perform();
546 * Simple click
548 public function click(): void
550 $this->webDriver->action()->click()->perform();
554 * Get element by byPartialLinkText
556 * @param string $partialLinkText The partial link text
558 public function byPartialLinkText(string $partialLinkText): RemoteWebElement
560 return $this->webDriver->findElement(WebDriverBy::partialLinkText($partialLinkText));
563 public function isSafari(): bool
565 $capabilities = $this->webDriver->getCapabilities();
567 return $capabilities !== null && mb_strtolower($capabilities->getBrowserName()) === 'safari';
571 * Get element by name
573 * @param string $name The name
575 public function byName(string $name): RemoteWebElement
577 return $this->webDriver->findElement(WebDriverBy::name($name));
581 * Checks whether the login is successful
583 public function isSuccessLogin(): bool
585 return $this->isElementPresent('xpath', '//*[@id="server-breadcrumb"]');
589 * Checks whether the login is unsuccessful
591 public function isUnsuccessLogin(): bool
593 return $this->isElementPresent('cssSelector', 'div #pma_errors');
597 * Used to go to the homepage
599 public function gotoHomepage(): void
601 $e = $this->byPartialLinkText('Server: ');
602 $e->click();
603 $this->waitAjax();
607 * Execute a database query
609 * @param string $query SQL Query to be executed
610 * @param Closure|null $onResults The function to call when the results are displayed
611 * @param Closure|null $afterSubmit The function to call after the submit button is clicked
613 * @throws Exception
615 public function dbQuery(string $query, Closure|null $onResults = null, Closure|null $afterSubmit = null): bool
617 $didSucceed = false;
618 $handles = null;
620 if (! $this->sqlWindowHandle) {
621 $this->webDriver->executeScript("window.open('about:blank','_blank');", []);
622 $this->webDriver->wait()->until(
623 WebDriverExpectedCondition::numberOfWindowsToBe(2),
625 $handles = $this->webDriver->getWindowHandles();
627 $lastWindow = end($handles);
628 $this->webDriver->switchTo()->window($lastWindow);
629 $this->login();
630 $this->sqlWindowHandle = $lastWindow;
633 if ($handles === null) {
634 $handles = $this->webDriver->getWindowHandles();
637 if ($this->sqlWindowHandle) {
638 $this->webDriver->switchTo()->window($this->sqlWindowHandle);
639 if (! $this->isSuccessLogin()) {
640 $this->takeScrenshot('SQL_window_not_logged_in');
642 return false;
645 $this->byXPath('//*[contains(@class,"nav-item") and contains(., "SQL")]')->click();
646 $this->waitAjax();
647 $this->typeInTextArea($query);
648 $this->scrollIntoView('button_submit_query');
649 $this->byId('button_submit_query')->click();
650 $afterSubmit?->call($this);
652 $this->waitAjax();
653 $this->waitForElement('className', 'result_query');
654 // If present then
655 $didSucceed = $this->isElementPresent('cssSelector', '.result_query .alert-success');
656 $onResults?->call($this);
659 reset($handles);
660 $lastWindow = current($handles);
661 $this->webDriver->switchTo()->window($lastWindow);
663 return $didSucceed;
666 public function takeScrenshot(string $comment): void
668 $screenshotDir = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR
669 . '..' . DIRECTORY_SEPARATOR . 'build' . DIRECTORY_SEPARATOR
670 . 'selenium';
671 if ($this->webDriver === null) {
672 return;
675 $key = time();
677 // This call will also create the file path
678 $this->webDriver->takeScreenshot(
679 $screenshotDir . DIRECTORY_SEPARATOR
680 . 'screenshot_' . $key . '_' . $comment . '.png',
682 $htmlOutput = $screenshotDir . DIRECTORY_SEPARATOR . 'source_' . $key . '.html';
683 file_put_contents($htmlOutput, $this->webDriver->getPageSource());
684 $testInfo = $screenshotDir . DIRECTORY_SEPARATOR . 'source_' . $key . '.json';
685 file_put_contents($testInfo, json_encode(
686 ['filesKey' => $key, 'testName' => $this->getTestName()],
687 JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES,
692 * Check if user is logged in to phpmyadmin
694 public function isLoggedIn(): bool
696 return $this->isElementPresent('xpath', '//*[@class="navigationbar"]');
700 * Perform a logout, if logged in
702 public function logOutIfLoggedIn(): void
704 if (! $this->isLoggedIn()) {
705 return;
708 $this->byCssSelector('img.icon.ic_s_loggoff')->click();
712 * Wait for an element to be present on the page
714 * @param string $func Locate using - cssSelector, xpath, tagName, partialLinkText, linkText, name, id, className
715 * @param string $arg Selector
717 public function waitForElement(string $func, string $arg): RemoteWebElement
719 $element = $this->webDriver->wait(30, 500)->until(
720 WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::$func($arg)),
722 self::assertInstanceOf(RemoteWebElement::class, $element);
724 return $element;
728 * Wait for an element to be present on the page or timeout
730 * @param string $func Locate using - cssSelector, xpath, tagName, partialLinkText, linkText, name, id, className
731 * @param string $arg Selector
732 * @param int $timeout Timeout in seconds
734 public function waitUntilElementIsPresent(string $func, string $arg, int $timeout): RemoteWebElement
736 $element = $this->webDriver->wait($timeout, 500)->until(
737 WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::$func($arg)),
739 self::assertInstanceOf(RemoteWebElement::class, $element);
741 return $element;
745 * Wait for an element to be visible on the page or timeout
747 * @param string $func Locate using - cssSelector, xpath, tagName, partialLinkText, linkText, name, id, className
748 * @param string $arg Selector
749 * @param int $timeout Timeout in seconds
751 public function waitUntilElementIsVisible(string $func, string $arg, int $timeout = 10): WebDriverElement
753 $element = $this->webDriver->wait($timeout, 500)->until(
754 WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::$func($arg)),
756 self::assertInstanceOf(WebDriverElement::class, $element);
758 return $element;
762 * Wait for an element to disappear
764 * @param string $func Locate using - byCss, byXPath, etc
765 * @param string $arg Selector
767 public function waitForElementNotPresent(string $func, string $arg): void
769 while (true) {
770 if (! $this->isElementPresent($func, $arg)) {
771 return;
774 usleep(5000);
779 * Check if element is present or not
781 * @param string $func Locate using - cssSelector, xpath, tagName, partialLinkText, linkText, name, id, className
782 * @param string $arg Selector
784 public function isElementPresent(string $func, string $arg): bool
786 try {
787 $this->webDriver->findElement(WebDriverBy::$func($arg));
788 } catch (NoSuchElementException | InvalidArgumentException | InvalidSelectorException) {
789 // Element not present
790 return false;
793 // Element Present
794 return true;
798 * Get table cell data by the ID of the table
800 * @param string $tableID Table identifier
801 * @param int $row Table row
802 * @param int $column Table column
804 * @return string text Data from the particular table cell
806 public function getCellByTableId(string $tableID, int $row, int $column): string
808 $sel = sprintf('table#%s tbody tr:nth-child(%d) td:nth-child(%d)', $tableID, $row, $column);
809 $element = $this->byCssSelector($sel);
810 $text = $element->getText();
812 return $text && is_string($text) ? trim($text) : '';
816 * Get table cell data by the class attribute of the table
818 * @param string $tableClass Class of the table
819 * @param int $row Table row
820 * @param int $column Table column
822 * @return string text Data from the particular table cell
824 public function getCellByTableClass(string $tableClass, int $row, int $column): string
826 $sel = sprintf('table.%s tbody tr:nth-child(%d) td:nth-child(%d)', $tableClass, $row, $column);
827 $element = $this->byCssSelector($sel);
828 $text = $element->getText();
830 return $text && is_string($text) ? trim($text) : '';
834 * Wrapper around keys method to not use it on not supported
835 * browsers.
837 * @param string $text Keys to send
839 public function keys(string $text): void
842 * Not supported in Safari Webdriver, see
843 * https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/4136
845 if ($this->isSafari()) {
846 self::markTestSkipped('Can not send keys to Safari browser.');
847 } else {
848 $this->webDriver->getKeyboard()->sendKeys($text);
853 * Wrapper around moveto method to not use it on not supported
854 * browsers.
856 * @param RemoteWebElement $element element
858 public function moveto(RemoteWebElement $element): void
861 * Not supported in Safari Webdriver, see
862 * https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/4136
864 if ($this->isSafari()) {
865 self::markTestSkipped('MoveTo not supported on Safari browser.');
866 } else {
867 $this->webDriver->getMouse()->mouseMove($element->getCoordinates());
872 * Wrapper around alertText method to not use it on not supported
873 * browsers.
875 public function alertText(): string
878 * Not supported in Safari Webdriver, see
879 * https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/4136
881 if (! $this->isSafari()) {
882 return $this->webDriver->switchTo()->alert()->getText();
885 self::markTestSkipped('Alerts not supported on Safari browser.');
889 * Type text in textarea (CodeMirror enabled)
891 * @param string $text Text to type
892 * @param int $index Index of CodeMirror instance to write to
894 public function typeInTextArea(string $text, int $index = 0): void
896 $this->waitForElement('cssSelector', 'div.cm-s-default');
897 $this->webDriver->executeScript(
898 "$('.cm-s-default')[" . $index . '].CodeMirror.setValue(' . json_encode($text) . ');',
903 * Accept alert
905 public function acceptAlert(): void
907 $this->webDriver->switchTo()->alert()->accept();
911 * Clicks the "More" link in the menu
913 public function expandMore(): void
915 // "More" menu is not displayed on large screens
916 if ($this->isElementPresent('cssSelector', 'li.nav-item.dropdown.d-none')) {
917 return;
920 // Not found, searching for another alternative
921 try {
922 $ele = $this->waitForElement('cssSelector', 'li.dropdown > a');
924 $ele->click();
925 $this->waitForElement('cssSelector', 'li.dropdown.show > a');
927 $this->waitUntilElementIsPresent('cssSelector', 'li.nav-item.dropdown.show > ul', 5000);
928 } catch (WebDriverException) {
929 return;
934 * Navigates browser to a table page.
936 * @param string $table Name of table
937 * @param bool $gotoHomepageRequired Go to homepage required
939 public function navigateTable(string $table, bool $gotoHomepageRequired = false): void
941 $this->navigateDatabase($this->databaseName, $gotoHomepageRequired);
943 // go to table page
944 $this->waitForElement('xpath', "//th//a[contains(., '" . $table . "')]")->click();
945 $this->waitAjax();
949 * Navigates browser to a database page.
951 * @param string $database Name of database
952 * @param bool $gotoHomepageRequired Go to homepage required
954 public function navigateDatabase(string $database, bool $gotoHomepageRequired = false): void
956 if ($gotoHomepageRequired) {
957 $this->gotoHomepage();
960 // Go to server databases
961 $this->waitForElement('partialLinkText', 'Databases')->click();
962 $this->waitAjax();
964 // go to specific database page
965 $this->waitForElement(
966 'xpath',
967 '//tr[(contains(@class, "db-row"))]//a[contains(., "' . $database . '")]',
968 )->click();
969 $this->waitAjax();
973 * Select an option that matches a value
975 * @param WebDriverElement $element The element
976 * @param string $value The value of the option
978 public function selectByValue(WebDriverElement $element, string $value): void
980 $select = new WebDriverSelect($element);
981 $select->selectByValue($value);
985 * Select an option that matches a text
987 * @param WebDriverElement $element The element
988 * @param string $text The text
990 public function selectByLabel(WebDriverElement $element, string $text): void
992 $select = new WebDriverSelect($element);
993 $select->selectByVisibleText($text);
997 * Scrolls to a coordinate such that the element with given id is visible
999 * @param string $elementId Id of the element
1000 * @param int $yOffset Offset from Y-coordinate of element
1002 public function scrollIntoView(string $elementId, int $yOffset = 70): void
1004 // 70pt offset by-default so that the topmenu does not cover the element
1005 $script = <<<'JS'
1006 const elementId = arguments[0];
1007 const yOffset = arguments[1];
1008 const position = document.getElementById(elementId).getBoundingClientRect();
1009 window.scrollBy({left: 0, top: position.top - yOffset, behavior: 'instant'});
1011 $this->webDriver->executeScript($script, [$elementId, $yOffset]);
1015 * Scrolls to a coordinate such that the element
1017 * @param WebDriverElement $element The element
1018 * @param int $xOffset The x offset to apply (defaults to 0)
1019 * @param int $yOffset The y offset to apply (defaults to 0)
1021 public function scrollToElement(WebDriverElement $element, int $xOffset = 0, int $yOffset = 0): void
1023 $script = <<<'JS'
1024 const leftValue = arguments[0];
1025 const topValue = arguments[1];
1026 window.scrollBy({left: leftValue, top: topValue, behavior: 'instant'});
1028 $this->webDriver->executeScript($script, [
1029 $element->getLocation()->getX() + $xOffset,
1030 $element->getLocation()->getY() + $yOffset,
1035 * Scroll to the bottom of page
1037 public function scrollToBottom(): void
1039 $script = <<<'JS'
1040 window.scrollTo({left: 0, top: document.body.scrollHeight, behavior: 'instant'});
1042 $this->webDriver->executeScript($script);
1046 * Reload the page
1048 public function reloadPage(): void
1050 $this->webDriver->executeScript('window.location.reload();');
1054 * Wait for AJAX completion
1056 public function waitAjax(): void
1058 /* Wait while code is loading */
1059 $this->webDriver->executeAsyncScript(
1060 'var callback = arguments[arguments.length - 1];'
1061 . 'function startWaitingForAjax() {'
1062 . ' if (! window.AJAX.active) {'
1063 . ' callback();'
1064 . ' } else {'
1065 . ' setTimeout(startWaitingForAjax, 200);'
1066 . ' }'
1067 . '}'
1068 . 'startWaitingForAjax();',
1073 * Wait for AJAX message disappear
1075 public function waitAjaxMessage(): void
1077 /* Get current message count */
1078 $ajaxMessageCount = $this->webDriver->executeScript('return window.getAjaxMessageCount();');
1079 /* Ensure the popup is gone */
1080 $this->waitForElementNotPresent('id', 'ajax_message_num_' . $ajaxMessageCount);
1084 * Tear Down function for test cases
1086 protected function tearDown(): void
1088 if (static::$createDatabase) {
1089 $this->dbQuery('DROP DATABASE IF EXISTS `' . $this->databaseName . '`;');
1092 if ($this->hadStorageDatabaseInstall) {
1093 $this->dbQuery('DROP DATABASE IF EXISTS `phpmyadmin`;');
1096 if ($this->status()->asString() !== 'success') {
1097 return;
1100 $this->markTestAs('passed', '');
1101 $this->sqlWindowHandle = null;
1102 $this->webDriver->quit();
1106 * Mark test as failed or passed on BrowserStack
1108 * @param string $status passed or failed
1109 * @param string $message a message
1111 private function markTestAs(string $status, string $message): void
1113 // If this is being run on Browerstack,
1114 // mark the test on Browerstack as failure
1115 if (! $this->hasBrowserstackConfig()) {
1116 return;
1119 $payload = json_encode(
1120 ['status' => $status, 'reason' => $message],
1122 $ch = curl_init();
1123 curl_setopt($ch, CURLOPT_URL, self::SESSION_REST_URL . $this->sessionId . '.json');
1124 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1125 curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
1126 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
1127 curl_setopt(
1128 $ch,
1129 CURLOPT_USERPWD,
1130 $this->getBrowserStackCredentials(),
1133 $headers = [];
1134 $headers[] = 'Content-Type: application/json';
1135 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
1137 curl_exec($ch);
1138 if (! curl_errno($ch)) {
1139 return;
1142 echo 'Error: ' . curl_error($ch) . "\n";
1145 private function getErrorVideoUrl(): void
1147 if (! $this->hasBrowserstackConfig()) {
1148 return;
1151 $ch = curl_init();
1152 curl_setopt($ch, CURLOPT_URL, self::SESSION_REST_URL . $this->sessionId . '.json');
1153 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1154 curl_setopt(
1155 $ch,
1156 CURLOPT_USERPWD,
1157 $this->getBrowserStackCredentials(),
1159 $result = curl_exec($ch);
1160 if (is_bool($result)) {
1161 echo 'Error: ' . curl_error($ch) . "\n";
1163 return;
1166 $proj = json_decode($result);
1167 if (is_object($proj) && property_exists($proj, 'automation_session')) {
1168 // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
1169 echo 'Test failed, get more information here: ' . $proj->automation_session->public_url . "\n";
1172 if (! curl_errno($ch)) {
1173 return;
1176 echo 'Error: ' . curl_error($ch) . "\n";
1180 * Mark unsuccessful tests as 'Failures' on Browerstack
1182 protected function onNotSuccessfulTest(Throwable $t): never
1184 if ($t instanceof SkippedTest) {
1185 parent::onNotSuccessfulTest($t);
1188 $this->markTestAs('failed', $t->getMessage());
1189 $this->takeScrenshot('test_failed');
1190 // End testing session
1191 $this->webDriver->quit();
1193 $this->sqlWindowHandle = null;
1195 $this->getErrorVideoUrl();
1197 // Call parent's onNotSuccessful to handle everything else
1198 parent::onNotSuccessfulTest($t);