Acceptance Testing IX - Final User Test

Final version of our user text for the MODX Manager


This is the final article on our user acceptance test. In the previous article, we created the Manager and User page objects containing the locators for the parts of the user form that we need to interact with in our test. In this one, we'll see the test itself.


MODX logo

This article assumes that you've installed and configured Composer, Codeception, PhpUnit, MODX, Java, WebDriver, and ChromeDriver as described in earlier articles, and that you've created the acceptance test support files from those articles.


Reviewing the Test

This test will perform the following tests in the MODX Manager:

  1. Log in
  2. Create users in the manager
  3. Verify that the user's data in the database
  4. Delete the users
  5. Confirm the deletion

The Test Code

Create the test file with this command in the _build directory (if it's not already there):

codecept generate:cest acceptance T15_ManagerUser

Edit the T15_ManagerUserCest.php file and replace everything with this code (also available at GitHub, here):

<?php
use Page\Acceptance\LoginPage;
use Page\Acceptance\ManagerPage;
use Page\Acceptance\UserPage;
use Codeception\Util\Fixtures;

class T15_ManagerUserCest
{
    /** @var modX $modx */
    protected $modx;
    protected $countries;

    protected $usernames = array('user1','user2');

    public function _before(AcceptanceTester $I)
    {
        $this->modx = Fixtures::get('modx');
        assertTrue($this->modx instanceof modX);

        $_country_lang = array();
        include $this->modx->getOption('core_path') .
            'lexicon/country/en.inc.php';
        $this->countries = $_country_lang;

        /* Make sure users are not there */
        foreach ($this->usernames as $username) {
            $user = $this->modx->getObject('modUser',
                array('username' => $username));
            if ($user) {
                $user->remove();
            }
        }
    }

    public function _after(AcceptanceTester $I) {
        /* Make sure users are removed */
        foreach ($this->usernames as $username) {
            $user = $this->modx->getObject('modUser',
                array('username' => $username));
            if ($user) {
              $user->remove();
            }
            $user = $this->modx->getObject('modUser',
                array('username' => $username));
            assertNull($user);
        }
    }

    // tests
    /** @throws Exception */
    public function CreateUsersTest(AcceptanceTester $I)
    {
        $I->wantTo('Log In');
        $generatedPassword = null;
        $loginPage = new LoginPage($I);
        $managerPage = new ManagerPage($I);
        $userPage = new UserPage($I);
        $loginPage->login();
        $I->see('Content');
        $I->see('Manage');

        $I->amGoingTo('Switch to User Panel');
        $I->waitForElementVisible($managerPage::$manageMenu);
        $I->moveMouseOver($managerPage::$manageMenu);
        $I->waitForElementVisible($managerPage::$manageUsersLink,30);
        $I->click($managerPage::$manageUsersLink);
        $I->wait(3);

        /* Prefix for fields with Id */
        $prefix = '#modx-user-';

        $users = $userPage::$users;
        foreach($users as $userFields) {
            $autoPassword = empty($userFields['password']);
            $I->waitForElementVisible($userPage::$newUserButton, 30);
            $I->click($userPage::$newUserButton);

            /* Fill and save user form for both users */
            foreach ($userFields as $locator => $value) {
                switch($locator) {
                    case 'country':
                        $I->click($userPage::$countryMenu);
                        $I->click("//div[contains(@class, 'x-combo-list-item')
                            and text() = '{$value}']");
                        break;

                    case 'gender':
                        $I->click($userPage::$genderMenu);
                        $I->waitForElementVisible("//div[contains(@class,
                            'x-combo-list-item') and
                            text() = '{$value}']", 10);
                        $I->click("//div[contains(@class, 'x-combo-list-item')
                            and text() = '{$value}']");
                        break;

                    case 'photo':
                        $I->fillField($userPage::$photoInput, $value);
                        break;

                    case 'password':
                        if (! $autoPassword) {
                            /* Manual password */
                            $I->click($userPage::$passwordNotifyScreen);
                            $I->click($userPage::$passwordGenManual);
                            $I->waitForElementVisible($userPage::$passwordInput, 5);
                            $I->wait(2); //necessary
                            $I->fillField($userPage::$passwordInput, $value);
                            $I->fillField($userPage::$passwordConfirmInput, $value);
                            $I->wait(1); // necessary
                        }
                        break;


                    default:
                        $I->fillField($prefix .
                            $locator, $value);
                        break;
                }
            }

            /* Save and close user form */
            $I->click($userPage::$userSaveButton);
            $I->waitForElementVisible($userPage::$userOkButton);
            if ($autoPassword) {
                $msg = $I->grabTextFrom($userPage::$autoPasswordValue);
                assertNotEmpty($msg);
                $msg = explode(':', $msg);
                $generatedPassword = trim($msg[1]);
            }
            $I->click($userPage::$userOkButton);

            /* Make sure user was saved, then close */
            $user = $this->modx->getObject('modUser',
                array('username' => $userFields['username']));
            assertInstanceOf('modUser', $user);
            $I->waitForElementVisible($userPage::$userCloseButton, 5);
            $I->wait(2); //necessary
            $I->click($userPage::$userCloseButton);
        }
        $loginPage->logout();
        /* Test database values for both users */
        foreach($users as $userFields) {
            $autoPassword = empty($userFields['password']);
            /** @var modUser $user */
            $user = $this->modx->getObject('modUser',
                array('username'=> $userFields['username']));
            assertInstanceOf('modUser', $user);
            $profile = $user->getOne('Profile');
            assertInstanceOf('modUserProfile', $profile);
            $dbFields = $user->toArray();
            $dbFields = array_merge($dbFields,
                $profile->toArray());
            foreach($userFields as $key => $value) {
                switch($key) {
                    case 'password':
                        if ($autoPassword) {
                           $pw = $generatedPassword;
                        } else {
                            $pw = $userFields['password'];
                        }
                        assertTrue($user->passwordMatches($pw));
                        break;

                    case 'country':
                        assertEquals($value,
                            $this->countries[strtolower($dbFields[$key])]);
                        break;

                    case 'dob':
                        assertEquals(strtotime($value),
                            $dbFields[$key]);
                        break;

                    case 'gender':
                        $gender = array(
                            '1' => 'Male',
                            '2' => 'Female',
                            '3' => 'Other',
                        );
                        assertEquals($value,
                            $gender[$dbFields['gender']]);
                        break;

                    case 'photo':
                        break;

                    default:
                        assertEquals($value,
                            $dbFields[$key]);
                        break;
                }
            }
        }
    }
}

The Top

At the top of the code, we have use statements for our Login, Manager, and User page objects as well as the code to let us to get the $modx object from the Fixtures array (put there in our _bootstrap.php file).

Next, in the _before() method, we remove the two users in case they are left over from an earlier failed run of the test.

Finally, in the _after() method, we remove them again (since our test will create them), and verify that they're gone.


Logging In

In the first part of the CreateUsersTest() method (our only test method), we set some variables, then we call the Login page object's login() method to log into the Manager and verify that we've made it.


Navigating to the Create/Edit Users Panel

Next, we use the Manager's Top Menu to go to Manage-> Users, and then click on the "New User" button to get to the Create/Edit User panel.


Creating the Users

After setting a few variables, we create the users in a loop using the $users variable from the User page object and the various locators at the top of that file. We use a switch statement to handle the user fields. Let's look at the default case first:

$I->fillField($prefix . $locator, $value);

This handles all the cases where the id of the test input is 'modx-user-fieldname'. We set the $prefix variable above to the string, #modx-user-. The part after that comes from the locator in the array of user fields. The value to be filled comes from the value associated with the locator key.

For example to fill the username field from this part of the user array:

'username' => 'User1,

We take the prefix (#modx-user-, tack on the key (username), and use the value (User1) as the second parameter:

$I->fillField(#modx-user-username, 'User1');

Anomalies

The method above works for all the fields that use a simple text input with the id attribute set to the field name. Unfortunately, that doesn't work for everything. The anomalies make up the rest of the cases in the switch statement.

For the country field, we need to click on the double arrow that triggers the dropdown list of counties ($userPage::countryMenu) and select the entry that matches the user's country (held in the $value variable). That selection is done with the XPath in this line:

$I->click("//div[contains(@class, 'x-combo-list-item')
    and text() = '{$value}']");

The XPath above looks for a div that has x-combo-list-item as one of its classes and text that matches the $value variable. Note that this locator is here rather than in the User page object, because it needs to plug in the current content of the $value variable.

The gender field works just like the country field.

The password field is a little more complicated.

case 'password':
    if (! $autoPassword) {
        /* Manual password */
        $I->click($userPage::$passwordNotifyScreen);
        $I->click($userPage::$passwordGenManual);
        $I->waitForElementVisible($userPage::$passwordInput, 5);
        $I->wait(2); //necessary
        $I->fillField($userPage::$passwordInput, $value);
        $I->fillField($userPage::$passwordConfirmInput, $value);
        $I->wait(1); // necessary
    }
    break;

First, we click on the option to show the password on the screen rather than sending it in an email.

Earlier, we set $autoPassword to true if the user's password field is empty. In that case, we don't need to do anything here, because MODX will generate the password. If $autoPassword is false, we need to select the option to create the password ourselves, then fill in the three password fields.

This section took some trial and error to get working. The two wait() lines are necessary (and the comments tell us so), as is the waitForElementVisible() call. It's a good practice to identify necessary wait() calls because you may have other wait() calls that are there just to slow things down so that you can see what's happening on the screen.

The photo field here is not correct, because I never got it to work. It's designed to let you browse the file system and select the photo. I'm fairly confident that I could make it work if I created a photo and browsed to it, but it was just too much work. Strangely, if you type or paste in the path to the photo file in the input here, it works fine, but if you put it there with $I->fillField(), you can see it filled on the screen, but it never makes it to the database.


Saving the User

Below the switch statement is the code to save the user and close the form. Clicking on the "Close" button after saving the user is necessary because otherwise, MODX automatically switches to the Update/User panel, and you're updating the user you just created rather than creating a new one.

Special action is required here if $autoPassword is true. In that case, we need to grab the password message, extract the actual password from it, and save the password for later user. This has to happen after we click on the "Save" button and before we click on the "OK" button in the popup and click on the "Close" button. We grab the password and save it to the $generatedPassword variable with this code:

if ($autoPassword) {
    $msg = $I->grabTextFrom($userPage::$autoPasswordValue);
    assertNotEmpty($msg);
    $msg = explode(':', $msg);
    $generatedPassword = trim($msg[1]);
}

Finally, we make sure the user was saved to the database, and click on the "Close" button.


Verifying the Users

The last section of the loop checks the values we provided for each user in the User page object against the actual values found in that user's record in the database.

We get the modUser and modUserProfile objects from the database and merge the values into a single array ($dbFields) with this code:

$user = $this->modx->getObject('modUser',
        array('username'=> $userFields['username']));
assertInstanceOf('modUser', $user);
$profile = $user->getOne('Profile');
assertInstanceOf('modUserProfile', $profile);
$dbFields = $user->toArray();
$dbFields = array_merge($dbFields,
    $profile->toArray());

Then we loop through the user's fields comparing the two in a switch statement. The default case is simple:

assertEquals($value, $dbFields[$key]);

As we saw before, there are some anomalies. If $autoPassword is true, we use the password we grabbed from the screen earlier ($generatedPassword). If not, we use the one from the User page object ($userFields['password']).

The password field from the database will be a hash value, so a literal comparison won't work. Instead, we used the passwordMatches() method of the modUser object. That all looks like this:

case 'password':
    if ($autoPassword) {
       $pw = $generatedPassword;
    } else {
        $pw = $userFields['password'];
    }
    assertTrue($user->passwordMatches($pw));
    break;

The country field is another anomaly. What's stored in the database is the two or three letter code for the country, so that won't match the full country name we used to set it. Earlier in the code, we "included" the lexicon/country/en.inc.php file, which provides the country name and code in an associative array, so we can do the comparison with this code:

assertEquals($value,
    $this->countries[strtolower($dbFields[$key])]);

For the dob (date of birth) field, we need to use strtotime() because the value in the database is stored as a timestamp:

assertEquals($value,
    $this->countries[strtolower($dbFields[$key])]);

The 'gender' field is also an anomaly because the value in the database is an integer (1, 2, or 3). We create our own array here to use for the comparison:

case 'gender':
    $gender = array(
        '1' => 'Male',
        '2' => 'Female',
        '3' => 'Other',
    );
    assertEquals($value,
        $gender[$dbFields['gender']]);
    break;

The final anomaly is the photo field. As I said earlier, I never got this to work. I put it in the array in the hope that one of you can make it work and let me know the method, so I can update the test code. For now, the case for this field is empty.


Closing Things Out

Notice that we've logged the user out when the work of creating the objects is finished. This is important. Forgetting to do that wouldn't affect running this test by itself, but would definitely cause trouble when running the entire acceptance suite. If the user is not logged out when the next test file runs, going to the login page ($I->amOnPage('self::$managerUrl');) would lead the user directly to the Manager page, since the user is still logged in. When the test tried to fill the username and password fields, it would fail because they're not there.


Running the Test

Now that we've created all the parts necessary for our test, you should be able to run it successfully by using this command in the _build directory:

codecept run acceptance T15_ManagerUserCest

Coming Up

In the next article we'll start working on a more ambitious test of the MODX resource security system.



For more information on how to use MODX to create a web site, see my web site Bob's Guides, or better yet, buy my book: MODX: The Official Guide.

Looking for high-quality, MODX-friendly hosting? As of May 2016, Bob's Guides is hosted at A2 hosting. (More information in the box below.)



Comments (0)


Please login to comment.

  (Login)