Rewrite Selenium tests with use of laravel/dusk

pull/7135/head
Aleksander Machniak 5 years ago
parent 47d9ed6d0c
commit 0b9ff6426e

@ -73,6 +73,11 @@ class rcmail extends rcube
static function get_instance($mode = 0, $env = '') static function get_instance($mode = 0, $env = '')
{ {
if (!self::$instance || !is_a(self::$instance, 'rcmail')) { if (!self::$instance || !is_a(self::$instance, 'rcmail')) {
// In cli-server mode env=test
if ($env === null && php_sapi_name() == 'cli-server') {
$env = 'test';
}
self::$instance = new rcmail($env); self::$instance = new rcmail($env);
// init AFTER object was linked with self::$instance // init AFTER object was linked with self::$instance
self::$instance->startup(); self::$instance->startup();

@ -0,0 +1,24 @@
<?php
namespace Tests\Browser\Addressbook;
class Addressbook extends \Tests\Browser\DuskTestCase
{
public function testAddressbook()
{
$this->browse(function ($browser) {
$this->go('addressbook');
// check task
$this->assertEnvEquals('task', 'addressbook');
$objects = $this->getObjects();
// these objects should be there always
$this->assertContains('qsearchbox', $objects);
$this->assertContains('folderlist', $objects);
$this->assertContains('contactslist', $objects);
$this->assertContains('countdisplay', $objects);
});
}
}

@ -0,0 +1,34 @@
<?php
namespace Tests\Browser\Addressbook;
class Import extends \Tests\Browser\DuskTestCase
{
public function testImport()
{
$this->browse(function ($browser) {
$this->go('addressbook', 'import');
// check task and action
$this->assertEnvEquals('task', 'addressbook');
$this->assertEnvEquals('action', 'import');
$objects = $this->getObjects();
// these objects should be there always
$this->assertContains('importform', $objects);
});
}
public function testImport2()
{
$this->browse(function ($browser) {
$this->go('addressbook', 'import');
$objects = $this->getObjects();
// these objects should be there always
$this->assertContains('importform', $objects);
});
}
}

@ -0,0 +1,190 @@
<?php
namespace Tests\Browser;
use PHPUnit\Framework\TestCase;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Chrome\SupportsChrome;
use Laravel\Dusk\Concerns\ProvidesBrowser;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process;
abstract class DuskTestCase extends TestCase
{
use ProvidesBrowser,
SupportsChrome;
protected $app;
protected static $phpProcess;
/**
* Prepare for Dusk test execution.
*
* @beforeClass
* @return void
*/
public static function prepare()
{
static::startWebServer();
static::startChromeDriver();
}
/**
* Create the RemoteWebDriver instance.
*
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
*/
protected function driver()
{
$options = (new ChromeOptions())->addArguments([
'--disable-gpu',
'--headless',
'--window-size=1280,720',
]);
return RemoteWebDriver::create(
'http://localhost:9515',
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY,
$options
)
);
}
/**
* Set up the test run
*
* @return void
*/
protected function setUp()
{
parent::setUp();
$this->app = \rcmail::get_instance();
Browser::$baseUrl = 'http://localhost:8000';
Browser::$storeScreenshotsAt = __DIR__ . '/screenshots';
Browser::$storeConsoleLogAt = __DIR__ . '/console';
// Purge screenshots from the last test run
$pattern = sprintf('failure-%s_%s-*',
str_replace("\\", '_', get_class($this)),
$this->getName(false)
);
try {
$files = Finder::create()->files()->in(__DIR__ . '/screenshots')->name($pattern);
foreach ($files as $file) {
@unlink($file->getRealPath());
}
}
catch (\Symfony\Component\Finder\Exception\DirectoryNotFoundException $e) {
// ignore missing screenshots directory
}
}
/**
* Assert specified rcmail.env value
*/
protected function assertEnvEquals($key, $expected)
{
$this->assertEquals($expected, $this->getEnv($key));
}
/**
* Get content of rcmail.env entry
*/
protected function getEnv($key)
{
$this->browse(function (Browser $browser) use ($key, &$result) {
$result = $browser->script("return rcmail.env['$key']");
$result = $result[0];
});
return $result;
}
/**
* Get HTML IDs of defined buttons for specified Roundcube action
*/
protected function getButtons($action)
{
$this->browse(function (Browser $browser) use ($action, &$buttons) {
$buttons = $browser->script("return rcmail.buttons['$action']");
$buttons = $buttons[0];
});
if (is_array($buttons)) {
foreach ($buttons as $idx => $button) {
$buttons[$idx] = $button['id'];
}
}
return (array) $buttons;
}
/**
* Return names of defined gui_objects
*/
protected function getObjects()
{
$this->browse(function (Browser $browser) use (&$objects) {
$objects = $browser->script("var i, r = []; for (i in rcmail.gui_objects) r.push(i); return r");
$objects = $objects[0];
});
return (array) $objects;
}
/**
* Log in the test user
*/
protected function doLogin()
{
$this->browse(function (Browser $browser) {
$browser->type('_user', TESTS_USER);
$browser->type('_pass', TESTS_PASS);
$browser->click('button[type="submit"]');
// wait after successful login
//$browser->waitForReload();
$browser->waitUntil('!rcmail.busy');
});
}
/**
* Visit specified task/action with logon if needed
*/
protected function go($task = 'mail', $action = null, $login = true)
{
$this->browse(function (Browser $browser) use ($task, $action, $login) {
$browser->visit("/?_task=$task&_action=$action");
// check if we have a valid session
if ($login && $this->getEnv('task') == 'login') {
$this->doLogin();
}
});
}
/**
* Starts PHP server.
*/
protected static function startWebServer()
{
$path = realpath(__DIR__ . '/../../public_html');
$cmd = ['php', '-S', 'localhost:8000'];
static::$phpProcess = new Process($cmd, null, []);
static::$phpProcess->setWorkingDirectory($path);
static::$phpProcess->start();
static::afterClass(function () {
static::$phpProcess->stop();
});
}
}

@ -0,0 +1,36 @@
<?php
namespace Tests\Browser;
class Login extends DuskTestCase
{
protected function setUp()
{
parent::setUp();
\bootstrap::init_db();
\bootstrap::init_imap();
}
public function testLogin()
{
// first test, we're already on the login page
$this->browse(function ($browser) {
$browser->visit('/');
$browser->assertTitleContains($this->app->config->get('product_name'));
// task should be set to 'login'
$this->assertEnvEquals('task', 'login');
$browser->assertVisible('#rcmloginuser');
$browser->assertVisible('#rcmloginpwd');
// test valid login
$this->go('mail');
// task should be set to 'mail' now
$this->assertEnvEquals('task', 'mail');
});
}
}

@ -0,0 +1,25 @@
<?php
namespace Tests\Browser;
class Logout extends DuskTestCase
{
public function testLogout()
{
$this->browse(function ($browser) {
$this->go('settings');
// wait for the menu and then click the Logout button
$browser->waitFor('#taskmenu');
$browser->click('#taskmenu a.logout');
// task should be set to 'login'
$this->assertEnvEquals('task', 'login');
// form should exist
$browser->assertVisible('input[name="_user"]');
$browser->assertVisible('input[name="_pass"]');
$browser->assertMissing('#taskmenu');
});
}
}

@ -0,0 +1,15 @@
<?php
namespace Tests\Browser\Mail;
class CheckRecent extends \Tests\Browser\DuskTestCase
{
public function testCheckRecent()
{
$this->browse(function ($browser) {
$this->go('mail');
// TODO
});
}
}

@ -0,0 +1,28 @@
<?php
namespace Tests\Browser\Mail;
class Compose extends \Tests\Browser\DuskTestCase
{
public function testCompose()
{
$this->browse(function ($browser) {
$this->go('mail', 'compose');
// check task and action
$this->assertEnvEquals('task', 'mail');
$this->assertEnvEquals('action', 'compose');
$objects = $this->getObjects();
// these objects should be there always
$this->assertContains('qsearchbox', $objects);
$this->assertContains('addressbookslist', $objects);
$this->assertContains('contactslist', $objects);
$this->assertContains('messageform', $objects);
$this->assertContains('attachmentlist', $objects);
$this->assertContains('filedrop', $objects);
$this->assertContains('uploadform', $objects);
});
}
}

@ -0,0 +1,37 @@
<?php
namespace Tests\Browser\Mail;
class Getunread extends \Tests\Browser\DuskTestCase
{
protected $msgcount = 0;
protected function setUp()
{
parent::setUp();
\bootstrap::init_imap();
\bootstrap::purge_mailbox('INBOX');
// import email messages
foreach (glob(TESTS_DIR . 'data/mail/list_*.eml') as $f) {
\bootstrap::import_message($f, 'INBOX');
$this->msgcount++;
}
}
public function testGetunread()
{
$this->browse(function ($browser) {
$this->go('mail');
// Folders list state
$browser->waitFor('.folderlist li.inbox.unread');
$this->assertEquals(strval($this->msgcount), $browser->text('.folderlist li.inbox span.unreadcount'));
// Messages list state
$this->assertCount($this->msgcount, $browser->elements('#messagelist tr.unread'));
});
}
}

@ -0,0 +1,35 @@
<?php
namespace Tests\Browser\Mail;
class MailList extends \Tests\Browser\DuskTestCase
{
protected function setUp()
{
parent::setUp();
\bootstrap::init_imap();
\bootstrap::purge_mailbox('INBOX');
// import email messages
foreach (glob(TESTS_DIR . 'data/mail/list_00.eml') as $f) {
\bootstrap::import_message($f, 'INBOX');
}
}
public function testList()
{
$this->browse(function ($browser) {
$this->go('mail');
$this->assertCount(1, $browser->elements('#messagelist tbody tr'));
// check message list
$browser->assertVisible('#messagelist tbody tr:first-child.unread');
$this->assertEquals('Lines', $browser->text('#messagelist tbody tr:first-child span.subject'));
//$browser->assertVisible('#messagelist tbody tr:first-child span.msgicon.unread');
});
}
}

@ -0,0 +1,26 @@
<?php
namespace Tests\Browser\Mail;
class Mail extends \Tests\Browser\DuskTestCase
{
public function testMail()
{
$this->browse(function ($browser) {
$this->go('mail');
// check task
$this->assertEnvEquals('task', 'mail');
$objects = $this->getObjects();
// these objects should be there always
$this->assertContains('qsearchbox', $objects);
$this->assertContains('mailboxlist', $objects);
$this->assertContains('messagelist', $objects);
$this->assertContains('quotadisplay', $objects);
$this->assertContains('search_filter', $objects);
$this->assertContains('countdisplay', $objects);
});
}
}

@ -0,0 +1,60 @@
In-Browser Tests
================
The idea of these testing suite is to make it as simple as possible to execute
the tests. So, you don't have to run any additional services, nor download
and install anything manually.
The tests are using [Laravel Dusk][laravel-dusk] and Chrome WebDriver.
PHP server is used to serve Roundcube instance on tests run.
INSTALLATION
------------
Installation:
1. Add `"laravel/dusk": "~5.7.0"` to your composer.json file and run `composer update`.
2. Install Chrome WebDriver for the version of Chrome/Chromium in your system. Yes,
you have to have Chrome/Chromium installed.
```
php tests/Browser/install.php [version]`
```
3. Configure the test account and Roundcube instance.
Create a config file named `config-test.inc.php` in the Roundcube config dir.
That file should provide specific `db_dsnw` and
`default_host` values for testing purposes as well as the credentials of a
valid IMAP user account used for running the tests with.
Add these config options used by the Browser tests:
```php
// Unit tests settings
$config['tests_username'] = 'roundcube.test@example.org';
$config['tests_password'] = '<test-account-password>';
```
WARNING
-------
Please note that the configured IMAP account as well as the Roundcube database
configred in `db_dsnw` will be wiped and filled with test data in every test
run. Under no circumstances you should use credentials of a production database
or email account!
Please, keep the file as simple as possible, i.e. containing only database
and imap/smtp settings needed for the test user authentication. We would
want to test default configuration. Especially only Elastic skin is supported.
EXECUTING THE TESTS
-------------------
To run the test suite call `phpunit` from the tests/Browser directory:
```
cd <roundcube-dir>/tests/Browser
phpunit # or ../../vendor/bin/phpunit
```
[laravel-dusk]: https://github.com/laravel/dusk

@ -0,0 +1,19 @@
<?php
namespace Tests\Browser\Settings;
class About extends \Tests\Browser\DuskTestCase
{
public function testAbout()
{
$this->browse(function ($browser) {
$this->go('settings', 'about');
// check task and action
$this->assertEnvEquals('task', 'settings');
$this->assertEnvEquals('action', 'about');
$browser->assertVisible('#pluginlist');
});
}
}

@ -0,0 +1,23 @@
<?php
namespace Tests\Browser\Settings;
class Folders extends \Tests\Browser\DuskTestCase
{
public function testFolders()
{
$this->browse(function ($browser) {
$this->go('settings', 'folders');
// task should be set to 'settings' and action to 'folders'
$this->assertEnvEquals('task', 'settings');
$this->assertEnvEquals('action', 'folders');
$objects = $this->getObjects();
// these objects should be there always
$this->assertContains('quotadisplay', $objects);
$this->assertContains('subscriptionlist', $objects);
});
}
}

@ -0,0 +1,22 @@
<?php
namespace Tests\Browser\Settings;
class Identities extends \Tests\Browser\DuskTestCase
{
public function testIdentities()
{
$this->browse(function ($browser) {
$this->go('settings', 'identities');
// check task and action
$this->assertEnvEquals('task', 'settings');
$this->assertEnvEquals('action', 'identities');
$objects = $this->getObjects();
// these objects should be there always
$this->assertContains('identitieslist', $objects);
});
}
}

@ -0,0 +1,20 @@
<?php
namespace Tests\Browser\Settings;
class Settings extends \Tests\Browser\DuskTestCase
{
public function testSettings()
{
$this->browse(function ($browser) {
$this->go('settings');
// task should be set to 'settings'
$this->assertEnvEquals('task', 'settings');
$objects = $this->getObjects();
$this->assertContains('sectionslist', $objects);
});
}
}

@ -0,0 +1,174 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Environment initialization script for functional tests |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
if (php_sapi_name() != 'cli')
die("Not in shell mode (php-cli)");
if (!defined('INSTALL_PATH')) define('INSTALL_PATH', realpath(__DIR__ . '/../../') . '/' );
require_once(INSTALL_PATH . 'program/include/iniset.php');
$rcmail = rcmail::get_instance(0, 'test');
define('TESTS_DIR', realpath(__DIR__) . '/');
define('TESTS_USER', $rcmail->config->get('tests_username'));
define('TESTS_PASS', $rcmail->config->get('tests_password'));
require_once(__DIR__ . '/DuskTestCase.php');
/**
* Utilities for test environment setup
*/
class bootstrap
{
static $imap_ready = null;
/**
* Wipe and re-initialize database
*/
public static function init_db()
{
$rcmail = rcmail::get_instance();
$dsn = rcube_db::parse_dsn($rcmail->config->get('db_dsnw'));
if ($dsn['phptype'] == 'mysql' || $dsn['phptype'] == 'mysqli') {
// drop all existing tables first
$db = $rcmail->get_dbh();
$db->query("SET FOREIGN_KEY_CHECKS=0");
$sql_res = $db->query("SHOW TABLES");
while ($sql_arr = $db->fetch_array($sql_res)) {
$table = reset($sql_arr);
$db->query("DROP TABLE $table");
}
// init database with schema
system(sprintf('cat %s %s | mysql -h %s -u %s --password=%s %s',
realpath(INSTALL_PATH . '/SQL/mysql.initial.sql'),
realpath(TESTS_DIR . 'data/mysql.sql'),
escapeshellarg($dsn['hostspec']),
escapeshellarg($dsn['username']),
escapeshellarg($dsn['password']),
escapeshellarg($dsn['database'])
));
}
else if ($dsn['phptype'] == 'sqlite') {
// delete database file -- will be re-initialized on first access
system(sprintf('rm -f %s', escapeshellarg($dsn['database'])));
}
}
/**
* Wipe the configured IMAP account and fill with test data
*/
public static function init_imap()
{
if (!TESTS_USER) {
return false;
}
else if (self::$imap_ready !== null) {
return self::$imap_ready;
}
self::connect_imap(TESTS_USER, TESTS_PASS);
self::purge_mailbox('INBOX');
self::ensure_mailbox('Archive', true);
return self::$imap_ready;
}
/**
* Authenticate to IMAP with the given credentials
*/
public static function connect_imap($username, $password, $host = null)
{
$rcmail = rcmail::get_instance();
$imap = $rcmail->get_storage();
if ($imap->is_connected()) {
$imap->close();
self::$imap_ready = false;
}
$imap_host = $host ?: $rcmail->config->get('default_host');
$a_host = parse_url($imap_host);
if ($a_host['host']) {
$imap_host = $a_host['host'];
$imap_ssl = isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'));
$imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
}
else {
$imap_port = 143;
$imap_ssl = false;
}
if (!$imap->connect($imap_host, $username, $password, $imap_port, $imap_ssl)) {
die("IMAP error: unable to authenticate with user " . TESTS_USER);
}
self::$imap_ready = true;
}
/**
* Import the given file into IMAP
*/
public static function import_message($filename, $mailbox = 'INBOX')
{
if (!self::init_imap()) {
die(__METHOD__ . ': IMAP connection unavailable');
}
$imap = rcmail::get_instance()->get_storage();
$imap->save_message($mailbox, file_get_contents($filename));
}
/**
* Delete all messages from the given mailbox
*/
public static function purge_mailbox($mailbox)
{
if (!self::init_imap()) {
die(__METHOD__ . ': IMAP connection unavailable');
}
$imap = rcmail::get_instance()->get_storage();
$imap->delete_message('*', $mailbox);
}
/**
* Make sure the given mailbox exists in IMAP
*/
public static function ensure_mailbox($mailbox, $empty = false)
{
if (!self::init_imap()) {
die(__METHOD__ . ': IMAP connection unavailable');
}
$imap = rcmail::get_instance()->get_storage();
$folders = $imap->list_folders();
if (!in_array($mailbox, $folders)) {
$imap->create_folder($mailbox, true);
}
else if ($empty) {
$imap->delete_message('*', $mailbox);
}
}
}

@ -0,0 +1,67 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Chrome WebDriver download tool |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
if (php_sapi_name() != 'cli')
die("Not in shell mode (php-cli)");
if (!defined('INSTALL_PATH')) define('INSTALL_PATH', realpath(__DIR__ . '/../../') . '/' );
require_once(INSTALL_PATH . 'program/include/iniset.php');
class Installer extends Laravel\Dusk\Console\ChromeDriverCommand
{
/**
* Execute the console command.
*
* @param string $version
*
* @return void
*/
public function install($version = null)
{
$version = trim($this->getUrl(sprintf($this->versionUrl, $version ?: $this->latestVersion())));
$currentOS = Laravel\Dusk\OperatingSystem::id();
foreach ($this->slugs as $os => $slug) {
if ($os === $currentOS) {
$archive = $this->download($version, $slug);
$binary = $this->extract($archive);
$this->rename($binary, $os);
}
}
echo "ChromeDriver binary successfully installed for version $version.\n";
}
/**
* Get the contents of a URL
*
* @param string $url URL
*
* @return string|bool
*/
protected function getUrl(string $url)
{
return file_get_contents($url);
}
}
$installer = new Installer;
$installer->install($argv[1]);

@ -2,28 +2,26 @@
bootstrap="bootstrap.php" bootstrap="bootstrap.php"
colors="true"> colors="true">
<testsuites> <testsuites>
<testsuite name="Mail"> <testsuite name="Logon">
<file>Login.php</file><!-- Login.php test must be first --> <file>Login.php</file>
<file>Mail/Mail.php</file> <file>Logout.php</file>
<file>Mail/CheckRecent.php</file>
<file>Mail/Compose.php</file>
<file>Mail/Getunread.php</file>
<file>Mail/List.php</file>
<file>Logout.php</file><!-- Logout.php test must be last -->
</testsuite> </testsuite>
<testsuite name="Addressbook"> <testsuite name="Addressbook">
<file>Login.php</file>
<file>Addressbook/Addressbook.php</file> <file>Addressbook/Addressbook.php</file>
<file>Addressbook/Import.php</file> <file>Addressbook/Import.php</file>
<file>Logout.php</file>
</testsuite> </testsuite>
<testsuite name="Settings"> <testsuite name="Settings">
<file>Login.php</file>
<file>Settings/About.php</file> <file>Settings/About.php</file>
<file>Settings/Folders.php</file> <file>Settings/Folders.php</file>
<file>Settings/Identities.php</file> <file>Settings/Identities.php</file>
<file>Settings/Settings.php</file> <file>Settings/Settings.php</file>
<file>Logout.php</file> </testsuite>
<testsuite name="Mail">
<file>Mail/Mail.php</file>
<file>Mail/CheckRecent.php</file>
<file>Mail/Compose.php</file>
<file>Mail/Getunread.php</file>
<file>Mail/List.php</file>
</testsuite> </testsuite>
</testsuites> </testsuites>
</phpunit> </phpunit>

@ -1,21 +0,0 @@
<?php
class Selenium_Addressbook_Addressbook extends Selenium_Test
{
public function testAddressbook()
{
$this->go('addressbook');
// check task
$env = $this->get_env();
$this->assertEquals('addressbook', $env['task']);
$objects = $this->get_objects();
// these objects should be there always
$this->assertContains('qsearchbox', $objects);
$this->assertContains('folderlist', $objects);
$this->assertContains('contactslist', $objects);
$this->assertContains('countdisplay', $objects);
}
}

@ -1,29 +0,0 @@
<?php
class Selenium_Addressbook_Import extends Selenium_Test
{
public function testImport()
{
$this->go('addressbook', 'import');
// check task and action
$env = $this->get_env();
$this->assertEquals('addressbook', $env['task']);
$this->assertEquals('import', $env['action']);
$objects = $this->get_objects();
// these objects should be there always
$this->assertContains('importform', $objects);
}
public function testImport2()
{
$this->go('addressbook', 'import');
$objects = $this->get_objects();
// these objects should be there always
$this->assertContains('importform', $objects);
}
}

@ -1,28 +0,0 @@
<?php
class Selenium_Login extends Selenium_Test
{
protected function setUp()
{
bootstrap::init_db();
bootstrap::init_imap();
parent::setUp();
}
public function testLogin()
{
// first test, we're already on the login page
$this->url(TESTS_URL);
// task should be set to 'login'
$env = $this->get_env();
$this->assertEquals('login', $env['task']);
// test valid login
$this->login();
// task should be set to 'mail' now
$env = $this->get_env();
$this->assertEquals('mail', $env['task']);
}
}

@ -1,20 +0,0 @@
<?php
class Selenium_Logout extends Selenium_Test
{
public function testLogout()
{
$this->go('mail');
$this->click_button('logout');
sleep(TESTS_SLEEP);
// task should be set to 'login'
$env = $this->get_env();
$this->assertEquals('login', $env['task']);
// form should exist
$user_input = $this->byCssSelector('form input[name="_user"]');
}
}

@ -1,14 +0,0 @@
<?php
class Selenium_Mail_CheckRecent extends Selenium_Test
{
public function testCheckRecent()
{
$this->go('mail');
$res = $this->ajaxResponse('check-recent', "rcmail.command('checkmail')");
$this->assertEquals('check-recent', $res['action']);
$this->assertRegExp('/this\.set_unread_count/', $res['exec']);
}
}

@ -1,25 +0,0 @@
<?php
class Selenium_Mail_Compose extends Selenium_Test
{
public function testCompose()
{
$this->go('mail', 'compose');
// check task and action
$env = $this->get_env();
$this->assertEquals('mail', $env['task']);
$this->assertEquals('compose', $env['action']);
$objects = $this->get_objects();
// these objects should be there always
$this->assertContains('qsearchbox', $objects);
$this->assertContains('addressbookslist', $objects);
$this->assertContains('contactslist', $objects);
$this->assertContains('messageform', $objects);
$this->assertContains('attachmentlist', $objects);
$this->assertContains('filedrop', $objects);
$this->assertContains('uploadform', $objects);
}
}

@ -1,37 +0,0 @@
<?php
class Selenium_Mail_Getunread extends Selenium_Test
{
protected $msgcount = 0;
protected function setUp()
{
parent::setUp();
bootstrap::init_imap();
bootstrap::purge_mailbox('INBOX');
// import email messages
foreach (glob(TESTS_DIR . 'Selenium/data/mail/list_*.eml') as $f) {
bootstrap::import_message($f, 'INBOX');
$this->msgcount++;
}
}
public function testGetunread()
{
$this->go('mail');
$res = $this->ajaxResponse('getunread', "rcmail.http_request('getunread')");
$this->assertEquals('getunread', $res['action']);
$env = $this->get_env();
$this->assertEquals($env['unread_counts']['INBOX'], $this->msgcount);
$li = $this->byCssSelector('.folderlist li.inbox');
$this->assertHasClass('unread', $li);
$badge = $this->byCssSelector('.folderlist li.inbox span.unreadcount');
$this->assertEquals(strval($this->msgcount), $this->getText($badge));
}
}

@ -1,50 +0,0 @@
<?php
class Selenium_Mail_List extends Selenium_Test
{
protected function setUp()
{
parent::setUp();
bootstrap::init_imap();
bootstrap::purge_mailbox('INBOX');
// import email messages
foreach (glob(TESTS_DIR . 'Selenium/data/mail/list_00.eml') as $f) {
bootstrap::import_message($f, 'INBOX');
}
}
public function testList()
{
$this->go('mail');
$res = $this->ajaxResponse('list', "rcmail.command('list')");
$this->assertEquals('list', $res['action']);
$this->assertRegExp('/this\.set_pagetitle/', $res['exec']);
$this->assertRegExp('/this\.set_unread_count/', $res['exec']);
$this->assertRegExp('/this\.set_rowcount/', $res['exec']);
$this->assertRegExp('/this\.set_message_coltypes/', $res['exec']);
$this->assertContains('current_page', $res['env']);
$this->assertContains('exists', $res['env']);
$this->assertContains('pagecount', $res['env']);
$this->assertContains('pagesize', $res['env']);
$this->assertContains('messagecount', $res['env']);
$this->assertContains('mailbox', $res['env']);
$this->assertEquals($res['env']['mailbox'], 'INBOX');
$this->assertEquals($res['env']['messagecount'], 1);
// check message list
$row = $this->byCssSelector('.messagelist tbody tr:first-child');
$this->assertHasClass('unread', $row);
$subject = $this->byCssSelector('.messagelist tbody tr:first-child td.subject');
$this->assertEquals('Lines', $this->getText($subject));
$icon = $this->byCssSelector('.messagelist tbody tr:first-child td.status span');
$this->assertHasClass('unread', $icon);
}
}

@ -1,23 +0,0 @@
<?php
class Selenium_Mail_Mail extends Selenium_Test
{
public function testMail()
{
$this->go('mail');
// check task
$env = $this->get_env();
$this->assertEquals('mail', $env['task']);
$objects = $this->get_objects();
// these objects should be there always
$this->assertContains('qsearchbox', $objects);
$this->assertContains('mailboxlist', $objects);
$this->assertContains('messagelist', $objects);
$this->assertContains('quotadisplay', $objects);
$this->assertContains('search_filter', $objects);
$this->assertContains('countdisplay', $objects);
}
}

@ -1,49 +0,0 @@
Running Selenium Tests
======================
In order to run the Selenium-based web tests, some configuration for the
Roundcube test instance need to be created. Along with the default config for a
given Roundcube instance, you should provide a config specifically for running
tests. To do so, create a config file named `config-test.inc.php` in the
regular Roundcube config dir. That should provide specific `db_dsnw` and
`default_host` values for testing purposes as well as the credentials of a
valid IMAP user account used for running the tests with.
Add these config options used by the Selenium tests:
```php
// Unit tests settings
$config['tests_username'] = 'roundcube.test@example.org';
$config['tests_password'] = '<test-account-password>';
$config['tests_url'] = 'http://localhost/roundcube/index-test.php';
```
The `tests_url` should point to Roundcube's index-test.php file accessible by
the Selenium web browser.
WARNING
-------
Please note that the configured IMAP account as well as the Roundcube database
configred in `db_dsnw` will be wiped and filled with test data in every test
run. Under no circumstances you should use credentials of a production database
or email account!
Run the tests
-------------
First you need to start a Selenium server. We recommend to use the
[Selenium Standalone Server][selenium-server] but the tests will also run on a
Selenium Grid. The tests are based in [PHPUnit_Selenium][phpunit] which can be
installed through [PEAR][pear-phpunit].
To start the test suite call `phpunit` from the Selenium directory:
```
cd <roundcube-dir>/tests/Selenium
phpunit
```
[phpunit]: http://phpunit.de/manual/4.0/en/selenium.html
[pear-phpunit]: http://pear.phpunit.de/
[selenium-server]: http://docs.seleniumhq.org/download/

@ -1,15 +0,0 @@
<?php
class Selenium_Settings_About extends Selenium_Test
{
public function testAbout()
{
$this->url(TESTS_URL . '?_task=settings&_action=about');
sleep(TESTS_SLEEP);
// check task and action
$env = $this->get_env();
$this->assertEquals('settings', $env['task']);
$this->assertEquals('about', $env['action']);
}
}

@ -1,20 +0,0 @@
<?php
class Selenium_Settings_Folders extends Selenium_Test
{
public function testFolders()
{
$this->go('settings', 'folders');
// task should be set to 'settings' and action to 'folders'
$env = $this->get_env();
$this->assertEquals('settings', $env['task']);
$this->assertEquals('folders', $env['action']);
$objects = $this->get_objects();
// these objects should be there always
$this->assertContains('quotadisplay', $objects);
$this->assertContains('subscriptionlist', $objects);
}
}

@ -1,19 +0,0 @@
<?php
class Selenium_Settings_Identities extends Selenium_Test
{
public function testIdentities()
{
$this->go('settings', 'identities');
// check task and action
$env = $this->get_env();
$this->assertEquals('settings', $env['task']);
$this->assertEquals('identities', $env['action']);
$objects = $this->get_objects();
// these objects should be there always
$this->assertContains('identitieslist', $objects);
}
}

@ -1,17 +0,0 @@
<?php
class Selenium_Settings_Settings extends Selenium_Test
{
public function testSettings()
{
$this->go('settings');
// task should be set to 'settings'
$env = $this->get_env();
$this->assertEquals('settings', $env['task']);
$objects = $this->get_objects();
$this->assertContains('sectionslist', $objects);
}
}

@ -1,348 +0,0 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Environment initialization script for unit tests |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
if (php_sapi_name() != 'cli')
die("Not in shell mode (php-cli)");
if (!defined('INSTALL_PATH')) define('INSTALL_PATH', realpath(__DIR__ . '/../../') . '/' );
define('TESTS_DIR', realpath(__DIR__ . '/../') . '/');
if (@is_dir(TESTS_DIR . 'config')) {
define('RCUBE_CONFIG_DIR', TESTS_DIR . 'config');
}
require_once(INSTALL_PATH . 'program/include/iniset.php');
// Extend include path so some plugin test won't fail
$include_path = ini_get('include_path') . PATH_SEPARATOR . TESTS_DIR . '..';
if (set_include_path($include_path) === false) {
die("Fatal error: ini_set/set_include_path does not work.");
}
$rcmail = rcmail::get_instance(0, 'test');
define('TESTS_URL', $rcmail->config->get('tests_url'));
define('TESTS_BROWSER', $rcmail->config->get('tests_browser', 'firefox'));
define('TESTS_USER', $rcmail->config->get('tests_username'));
define('TESTS_PASS', $rcmail->config->get('tests_password'));
define('TESTS_SLEEP', $rcmail->config->get('tests_sleep', 5));
PHPUnit_Extensions_Selenium2TestCase::shareSession(true);
/**
* satisfy PHPUnit
*/
class bootstrap
{
static $imap_ready = null;
/**
* Wipe and re-initialize (mysql) database
*/
public static function init_db()
{
$rcmail = rcmail::get_instance();
$dsn = rcube_db::parse_dsn($rcmail->config->get('db_dsnw'));
if ($dsn['phptype'] == 'mysql' || $dsn['phptype'] == 'mysqli') {
// drop all existing tables first
$db = $rcmail->get_dbh();
$db->query("SET FOREIGN_KEY_CHECKS=0");
$sql_res = $db->query("SHOW TABLES");
while ($sql_arr = $db->fetch_array($sql_res)) {
$table = reset($sql_arr);
$db->query("DROP TABLE $table");
}
// init database with schema
system(sprintf('cat %s %s | mysql -h %s -u %s --password=%s %s',
realpath(INSTALL_PATH . '/SQL/mysql.initial.sql'),
realpath(TESTS_DIR . 'Selenium/data/mysql.sql'),
escapeshellarg($dsn['hostspec']),
escapeshellarg($dsn['username']),
escapeshellarg($dsn['password']),
escapeshellarg($dsn['database'])
));
}
else if ($dsn['phptype'] == 'sqlite') {
// delete database file -- will be re-initialized on first access
system(sprintf('rm -f %s', escapeshellarg($dsn['database'])));
}
}
/**
* Wipe the configured IMAP account and fill with test data
*/
public static function init_imap()
{
if (!TESTS_USER) {
return false;
}
else if (self::$imap_ready !== null) {
return self::$imap_ready;
}
self::connect_imap(TESTS_USER, TESTS_PASS);
self::purge_mailbox('INBOX');
self::ensure_mailbox('Archive', true);
return self::$imap_ready;
}
/**
* Authenticate to IMAP with the given credentials
*/
public static function connect_imap($username, $password, $host = null)
{
$rcmail = rcmail::get_instance();
$imap = $rcmail->get_storage();
if ($imap->is_connected()) {
$imap->close();
self::$imap_ready = false;
}
$imap_host = $host ?: $rcmail->config->get('default_host');
$a_host = parse_url($imap_host);
if ($a_host['host']) {
$imap_host = $a_host['host'];
$imap_ssl = isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'));
$imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
}
else {
$imap_port = 143;
$imap_ssl = false;
}
if (!$imap->connect($imap_host, $username, $password, $imap_port, $imap_ssl)) {
die("IMAP error: unable to authenticate with user " . TESTS_USER);
}
self::$imap_ready = true;
}
/**
* Import the given file into IMAP
*/
public static function import_message($filename, $mailbox = 'INBOX')
{
if (!self::init_imap()) {
die(__METHOD__ . ': IMAP connection unavailable');
}
$imap = rcmail::get_instance()->get_storage();
$imap->save_message($mailbox, file_get_contents($filename));
}
/**
* Delete all messages from the given mailbox
*/
public static function purge_mailbox($mailbox)
{
if (!self::init_imap()) {
die(__METHOD__ . ': IMAP connection unavailable');
}
$imap = rcmail::get_instance()->get_storage();
$imap->delete_message('*', $mailbox);
}
/**
* Make sure the given mailbox exists in IMAP
*/
public static function ensure_mailbox($mailbox, $empty = false)
{
if (!self::init_imap()) {
die(__METHOD__ . ': IMAP connection unavailable');
}
$imap = rcmail::get_instance()->get_storage();
$folders = $imap->list_folders();
if (!in_array($mailbox, $folders)) {
$imap->create_folder($mailbox, true);
}
else if ($empty) {
$imap->delete_message('*', $mailbox);
}
}
}
// @TODO: make sure mailbox has some content (always the same) or is empty
// @TODO: plugins: enable all?
/**
* Base class for all tests in this directory
*/
class Selenium_Test extends PHPUnit_Extensions_Selenium2TestCase
{
protected $login_data = null;
protected function setUp()
{
$this->setBrowser(TESTS_BROWSER);
$this->login_data = array(TESTS_USER, TESTS_PASS);
// Set root to our index.html, for better performance
// See https://github.com/sebastianbergmann/phpunit-selenium/issues/217
$baseurl = preg_replace('!/index(-.+)?\.php^!', '', TESTS_URL);
$this->setBrowserUrl($baseurl . '/tests/Selenium');
}
protected function login($username = null, $password = null)
{
if (!empty($username)) {
$this->login_data = array($username, $password);
}
$this->go('mail', null, true);
}
protected function do_login()
{
$user_input = $this->byCssSelector('form input[name="_user"]');
$pass_input = $this->byCssSelector('form input[name="_pass"]');
$submit = $this->byCssSelector('form input[type="submit"]');
$user_input->value($this->login_data[0]);
$pass_input->value($this->login_data[1]);
// submit login form
$submit->click();
// wait after successful login
sleep(TESTS_SLEEP);
}
protected function go($task = 'mail', $action = null, $login = true)
{
$this->url(TESTS_URL . '?_task=' . $task);
// wait for interface load (initial ajax requests, etc.)
sleep(TESTS_SLEEP);
// check if we have a valid session
$env = $this->get_env();
if ($login && $env['task'] == 'login') {
$this->do_login();
}
if ($action) {
$this->click_button($action);
sleep(TESTS_SLEEP);
}
}
protected function get_env()
{
return $this->execute(array(
'script' => 'return window.rcmail ? rcmail.env : {};',
'args' => array(),
));
}
protected function get_buttons($action)
{
$buttons = $this->execute(array(
'script' => "return rcmail.buttons['$action'];",
'args' => array(),
));
if (is_array($buttons)) {
foreach ($buttons as $idx => $button) {
$buttons[$idx] = $button['id'];
}
}
return (array) $buttons;
}
protected function get_objects()
{
return $this->execute(array(
'script' => "var i,r = []; for (i in rcmail.gui_objects) r.push(i); return r;",
'args' => array(),
));
}
protected function click_button($action)
{
$buttons = $this->get_buttons($action);
$id = array_shift($buttons);
// this doesn't work for me
$this->byId($id)->click();
}
protected function ajaxResponse($action, $script = '', $button = false)
{
if (!$script && !$button) {
$script = "rcmail.command('$action')";
}
$script =
"if (!window.test_ajax_response) {
window.test_ajax_response_object = {};
function test_ajax_response(response)
{
if (response.response && response.response.action) {
window.test_ajax_response_object[response.response.action] = response.response;
}
}
rcmail.addEventListener('responsebefore', test_ajax_response);
}
window.test_ajax_response_object['$action'] = null;
$script;
";
// run request
$this->execute(array(
'script' => $script,
'args' => array(),
));
if ($button) {
$this->click_button($action);
}
// wait
sleep(TESTS_SLEEP);
// get response
$response = $this->execute(array(
'script' => "return window.test_ajax_response_object ? test_ajax_response_object['$action'] : {};",
'args' => array(),
));
return $response;
}
protected function getText($element)
{
return $element->text() ?: $element->attribute('textContent');
}
protected function assertHasClass($classname, $element)
{
$this->assertContains($classname, $element->attribute('class'));
}
}

@ -1,8 +0,0 @@
<html>
<head>
<title>Roundcube Webmail Tests</title>
</head>
<body>
Testing...
</body>
</html>
Loading…
Cancel
Save