Files
roundcubemail/tests/Browser/Browser.php
2025-06-17 14:38:52 +02:00

366 lines
9.2 KiB
PHP

<?php
namespace Roundcube\Tests\Browser;
use Facebook\WebDriver\Chrome\ChromeDevToolsDriver;
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Exception\StaleElementReferenceException;
use Facebook\WebDriver\Exception\TimeoutException;
use Facebook\WebDriver\WebDriverKeys;
use PHPUnit\Framework\Assert;
/**
* Laravel Dusk Browser extensions
*/
class Browser extends \Laravel\Dusk\Browser
{
/**
* Assert number of (visible) elements
*/
public function assertElementsCount($selector, $expected_count, $visible = true)
{
$elements = $this->elements($selector);
$count = count($elements);
if ($visible) {
foreach ($elements as $element) {
if (!$element->isDisplayed()) {
$count--;
}
}
}
Assert::assertEquals($expected_count, $count);
return $this;
}
/**
* Assert specified rcmail.env value
*/
public function assertEnvEquals($key, $expected)
{
Assert::assertEquals($expected, $this->getEnv($key));
return $this;
}
/**
* Assert specified checkbox state
*/
public function assertCheckboxState($selector, $state)
{
if ($state) {
$this->assertChecked($selector);
} else {
$this->assertNotChecked($selector);
}
return $this;
}
/**
* Assert that the given element has specified class assigned.
*/
public function assertHasClass($selector, $class_name)
{
$fullSelector = $this->resolver->format($selector);
$element = $this->resolver->findOrFail($selector);
$classes = explode(' ', (string) $element->getAttribute('class'));
Assert::assertContains($class_name, $classes);
return $this;
}
/**
* Assert Task menu state
*/
public function assertTaskMenu($selected)
{
$this->with(new Components\Taskmenu(), static function ($browser) use ($selected) {
$browser->assertMenuState($selected);
});
return $this;
}
/**
* Assert toolbar menu state
*/
public function assertToolbarMenu($active, $disabled = [], $missing = [])
{
$this->with(new Components\Toolbarmenu(), static function ($browser) use ($active, $disabled, $missing) {
$browser->assertMenuState($active, $disabled, $missing);
});
return $this;
}
/**
* Close toolbar menu (on phones)
*/
public function closeToolbarMenu()
{
$this->with(new Components\Toolbarmenu(), static function ($browser) {
$browser->closeMenu();
});
return $this;
}
/**
* Select taskmenu item
*/
public function clickTaskMenuItem($name)
{
$this->with(new Components\Taskmenu(), static function ($browser) use ($name) {
$browser->clickMenuItem($name);
});
return $this;
}
/**
* Select toolbar menu item
*/
public function clickToolbarMenuItem($name, $dropdown_action = null, $close = true)
{
$this->with(new Components\Toolbarmenu(), static function ($browser) use ($name, $dropdown_action, $close) {
$browser->clickMenuItem($name, $dropdown_action, $close);
});
return $this;
}
/**
* Shortcut to click an element while holding CTRL key
*/
public function ctrlClick($selector)
{
$this->driver->getKeyboard()->pressKey(WebDriverKeys::LEFT_CONTROL);
$this->element($selector)->click();
$this->driver->getKeyboard()->releaseKey(WebDriverKeys::LEFT_CONTROL);
return $this;
}
/**
* Return rcmail.env entry
*/
public function getEnv($key)
{
return $this->driver->executeScript("return rcmail.env['{$key}']");
}
/**
* Visit specified task/action with logon if needed
*/
public function go($task = 'mail', $action = null, $login = true)
{
$this->with(new Components\App(), static function ($browser) use ($task, $action, $login) {
$browser->gotoAction($task, $action, $login);
});
return $this;
}
/**
* Check if in Phone mode
*/
public static function isPhone()
{
return getenv('TESTS_MODE') == 'phone';
}
/**
* Check if in Tablet mode
*/
public static function isTablet()
{
return getenv('TESTS_MODE') == 'tablet';
}
/**
* Check if in Desktop mode
*/
public static function isDesktop()
{
return !self::isPhone() && !self::isTablet();
}
/**
* Handler for actions that expect to open a new window
*
* @param callable $callback Function to execute with Browser object as argument
*
* @return array Main window handle and new window handle
*/
public function openWindow($callback)
{
$current_window = $this->driver->getWindowHandle();
$before_handles = $this->driver->getWindowHandles();
$callback($this);
$after_handles = $this->driver->getWindowHandles();
$new_window = array_first(array_diff($after_handles, $before_handles));
return [$current_window, $new_window];
}
/**
* Change state of the Elastic's pretty checkbox
*/
public function setCheckboxState($selector, $state)
{
// Because you can't operate on the original checkbox directly
$this->ensurejQueryIsAvailable();
if ($state) {
$run = "if (!element.prev().is(':checked')) element.click()";
} else {
$run = "if (element.prev().is(':checked')) element.click()";
}
$this->script(
"var element = jQuery('{$selector}')[0] || jQuery('input[name={$selector}]')[0];"
. "element = jQuery(element).next('.custom-control-label'); {$run};"
);
return $this;
}
/**
* Returns content of a downloaded file
*/
public function readDownloadedFile($filename)
{
$filename = $this->getDownloadedFilePath($filename);
// Give the browser a chance to finish download
$n = 0;
while (!file_exists($filename)) {
if ($n++ > 5) {
break;
}
sleep(1);
}
Assert::assertFileExists($filename);
return file_get_contents($filename);
}
/**
* Removes downloaded file
*/
public function removeDownloadedFile($filename)
{
@unlink($this->getDownloadedFilePath($filename));
return $this;
}
/**
* Close UI (notice/confirmation/loading/error/warning) message
*/
public function closeMessage($type)
{
$selector = '#messagestack > div.' . $type;
$this->click($selector);
return $this;
}
/**
* Wait for the given selector to be removed.
*
* @param string $selector
* @param int|null $seconds
*
* @return $this
*
* @throws TimeoutException
*/
public function waitUntilMissingOrStale($selector, $seconds = null)
{
$message = $this->formatTimeOutMessage('Waited %s seconds for removal of selector', $selector);
return $this->waitUsing($seconds, 100, function () use ($selector) {
try {
$missing = !$this->resolver->findOrFail($selector)->isDisplayed();
} catch (NoSuchElementException $e) {
$missing = true;
} catch (StaleElementReferenceException $e) {
$missing = true;
}
return $missing;
}, $message);
}
/**
* Wait until the UI is unlocked
*/
public function waitUntilNotBusy()
{
$this->waitUntil('!rcmail.busy');
return $this;
}
/**
* Wait for UI (notice/confirmation/loading/error/warning) message
* and assert it's text
*/
public function waitForMessage($type, $text)
{
$selector = '#messagestack > div.' . $type;
$this->waitFor($selector)->assertSeeIn($selector, $text);
return $this;
}
/**
* Execute code within body context.
* Useful to execute code that selects elements outside of a component context
*/
public function withinBody($callback)
{
if ($this->resolver->prefix != 'body') {
$orig_prefix = $this->resolver->prefix;
$this->resolver->prefix = 'body';
}
call_user_func($callback, $this);
if (isset($orig_prefix)) {
$this->resolver->prefix = $orig_prefix;
}
return $this;
}
public function getDownloadedFilePath($filename)
{
$basedir = getenv('TESTRUNNER_DOWNLOADS_DIR') ?: TESTS_DIR . 'downloads';
$basedir = rtrim($basedir, \DIRECTORY_SEPARATOR);
return $basedir . \DIRECTORY_SEPARATOR . $filename;
}
public function printToPDF($driver) {
$devTools = new ChromeDevToolsDriver($driver);
$result = $devTools->execute(
'Page.printToPDF',
[
'printBackground' => true,
'scale' => 1,
]
);
return base64_decode($result['data']);
}
}