Tuesday, September 14, 2010

Prepare objects repository using Selenium UI-Element feature

In my previous post we have recorded simple test case to ensure that new entity object can be created via web application. As you observed using original objects description defined by Selenium IDE represents test scenario in not human readable format. Also, it is hard to maintain. Therefore we will use UI-elements. From this post such objects as Pagesets and UI Elements to be specified before we can start using this feature in our test cases.

Pagesets shorthand

I’ve looked thru the application and determine next pages to work with: allPages (it contains objects related to any page), mainPage (there is the only page in the application that any user may have access to with no authentication) and areaPage. This page includes shorthand for all objects related to area entity.

applMap.addPageset({
    name: 'allPages'
    , description: 'all appl.ua pages'
    , pathRegexp: '.*'
});

 
applMap.addPageset({
    name: 'areaPages'
    , description: 'all pages for Areas'
    , pathRegexp: 'member\.php\?lang=(eng|ukr)&op=' + page_ids['Areas']
});

 
applMap.addPageset({
    name: 'mainPages'
    , description: 'home pages for a user who is not logged in'
    , pathRegexp: 'appl/(eng|ukr)/home'
});
Regular expression in areaPages description includes some list (pairs of possible page id with corresponding name). With this list it is easy to make such object descriptions. For instance, choosing areas page you do not need to know exact id value of this page, just select ‘Areas’ item from the list. page_ids list represents key/value pairs. Below you may find an example of this list:

var page_ids = {
    'Dashboard': 'page_dashboard'
    , 'Areas': 'list_areas'
    …
    , 'Activities': 'list_activity'
};

In shorthand for mainPages above you may see the regular expression contains ‘(eng|ukr)’ value. It means we make this object in the repository language independent. It means that the page will be discovered regardless locale selected at the moment.

So, meaningful pages are specified. Remaining pages can be covered the same way. The only thing left is to add object descriptions for certain entities and link them with pages from the set.

UI-Elements shorthand

I will describe objects to be used in this blog below (like credentials, menu items, table elements, popup dialog box etc). Others objects can be covered using similar approach. The following description shows how to specify credentials.

  • Credentials:

applMap.addElement('mainPages', {
    name: 'credentials'
    , description: 'Credentials properties'
    , args: [
        {
            name: 'credential_name'
            , description: 'the name of the credential'
            , defaultValues: keys(credential_ids)
        }
    ]
    , getLocator: function(args) {
        var topic = credential_ids[args.credential_name];
        return "//input[@name='" + topic + "']";
    }
});

In this example credential_ids there is nothing else but set of key/value. Wit this approach you can make object description language independent.

var credential_ids = {
    'User': 'username'
    , 'Password': 'password'
    , 'OK': 'imageField'
}


Login page

  • Menu items:

applMap.addElement('allPages', {
    name: 'menuitem'
    , description: 'Manage menuitem'
    , args: [
        {
            name: 'submenu'
            , description: 'the name of the submenu'
            , defaultValues: keys(menuitem_ids)
        }
    ]
    , getLocator: function(args) {
        var topic = menuitem_ids[args.submenu];
        return "//td[@id='menuItem_" + topic + "']/table/tbody/tr/td[2]/table/tbody/tr/td[1]";
    }
});

where menuitem_ids is:

var menuitems = [
    'Dashboard',
    'Manage',
    …
    'Language',
    'Help',
    'Logout'
];

Menu items

  • Table elements:

Table consists of several objects such as cells, column headers, filters etc. Cell intended to hold respective values. Clicking on the headers sort order can be specified for elements in the table. With filters search criteria can be determined. If some filters are not empty – all rows with values in corresponding columns that matched the criteria to be shown.

  • Cells:


applMap.addElement('allPages', {
    name: 'gridbox_cell'
    , description: 'A cell in the common gridbox'
    , args: [
        {
            name: 'row_index'
            , description: 'the index of the row'
            , defaultValues: range(2, 20)
        }
        , {
            name: 'column_index'
            , description: 'the index of the column'
            , defaultValues: range(1, 20)
        }
    ]
    , getLocator: function(args) {
        var xpath = "//div[@id='gridboxObjectBuffer']/table/tbody/tr[" + (args.row_index || 1) + "]/td[" + (args.column_index || 1) + "]";
        return xpath;
    }
});

This description is parameterized. Every cell can be defined using row and column ids.

  • Column headers:

Concerning column headers - two ways on how to get certain headers are shown below. First one implemented the way using column name while the second one – its id. You are free to use any of these ways depending on your needs. I admit that the second way has an issue – respective description is language dependent. It works fine for one language only. If you are going to test some application on different locales – this description should be updated in order to cover this case. Let’s keep it as is since it is minor, and respective solution has already been described above. I’m just adding this to the outstanding issues list.

applMap.addElement('allPages', {
    name: 'gridbox_header_by_index'
    , description: 'Column in the header of the common gridbox'
    , args: [
        {
            name: 'column_index'
            , description: "the index of the column in the gridbox header"
            , defaultValues: range(1, 20)
        }
    ]
    , getLocator: function(args) {
        var xpath = "//div[@id='gridboxHeader']/table/tbody/tr/td[1]/table/tbody/tr[2]/td[" + (args.column_index || 1) + "]/div";
        return xpath;
    }
});

applMap.addElement('allPages', {
    name: 'gridbox_header_by_name'
    , description: 'Column in the header of the common gridbox'
    , args: [
        {
            name: 'column_name'
            , description: "the index of the column in the gridbox header"
            , defaultValues: headers
        }
    ]
    , getLocator: function(args) {
        var xpath = "//div[@id='gridboxHeader']/table/tbody/tr/td[1]/table/tbody/tr/td/div[contains(text(),'" + args.column_name + "')]";
        return xpath;
    }
});

Filter description is nothing else but an object related to every column and can be reached by its id:

applMap.addElement('allPages', {
    name: 'gridbox_filter_criteria'
    , description: 'Filter criteria in the header of the common gridbox'
    , args: [
        {
            name: 'column_index'
            , description: "Text area for the filter criteria for the column in the gridbox header"
            , defaultValues: range(2, 20)
        }
    ]
    , getLocator: function(args) {
        var xpath = "//div[@id='gridboxHeader']/table/tbody/tr/td[1]/table/tbody/tr[3]/td[" + (args.column_index || 1) + "]/input";
        return xpath;
    }
});

Area entity objects list

  • Dialog box:


Any entity object can be registered/updated via respective popup dialog box. Area entity has such dialog window as well as others entities. This box includes primary and auxiliary properties (in our particular case located on General and Customer places tabs correspondingly). Also, there few button located on this box.

applMap.addElement('areaPages', {
    name: 'tabs_in_dialog_box'
    , description: "Tabs located in dialog box"
    , args: [
        {
            name: 'name'
            , description: 'the name of the tab'
            , defaultValues: keys(tab_ids)
        }
    ]
    , getLocator: function(args) {
        var xpath = "//div[@id='a_tabbar']/div/div[1]/div/div[" + tab_ids[args.name] + "]/div[3]/div";
        return xpath;
    }
});

applMap.addElement('areaPages', {
    name: 'dialog_box_area_property'
    , description: 'Filter criteria in the header of the dialog box'
    , args: [
        {
            name: 'property_name'
            , description: "Property for selected area in respective dialog box"
            , defaultValues: keys(area_property_ids)
        }
    ]
    , getLocator: function(args) {
        var xpath = "" + area_property_ids[args.property_name] + "";
        return xpath;
    }
});

Using the description above you may reach any property on General tab (i.e. Area name, Description and Other information). area_property_ids set of key/value lists possible items:

var area_property_ids = {
    'Area name': 'def'
    , 'Description': 'DESCRIPTION'
    , 'Other information': 'OTHERINFO'
}

applMap.addElement('areaPages', {
    name: 'buttons_in_dialog_box'
    , description: "Buttons located in dialog box appeared on top for an object"
    , args: [
        {
            name: 'name'
            , description: 'the name of the button'
            , defaultValues: [
                'Submit'
                , 'Close'
                , 'Check'
                , 'Check all'
                , 'Uncheck'
                , 'Uncheck all'
            ]
        }
    ]
    , getLocator: function(args) {
        var xpath = "//input[@type='button' and @value='" + args.name + "']";
        return xpath;
    }
});

var tab_ids = {
    'General': '1'
    , 'Customer places': '2'
}

It seems all objects required to record our simple scenario have been described using this wonderful UI-Element locator plug-in for Selenium IDE. Without this plug-in it will be too hard to make the automation readable and maintainable.


Dialog box for area entity object


  • Buttons in the dialog box:

Oh, forgot about one more object – this is that button which calls dialog box to register new area object. Here it is:

applMap.addElement('areaPages', {
    name: 'buttons_under_menu'
    , description: "Buttons located under main menu on top common grid"
    , args: [
        {
            name: 'name'
            , description: 'the name of the button'
            , defaultValues: button_names
        }
    ]
    , getLocator: function(args) {
        var xpath = "//td/table[contains(@title,'" + args.name + "')]";
        return xpath;
    }
});

var button_names = [
    'Add new area (Ins)'
    , 'Edit (Enter)'
    …
];

Initially, I was going to describe object repository and update simple test case in scope of one post. However as I can see it became to long. So it would be better to divide the material on two separate posts. Please see the updated test case in my later post.

Monday, September 6, 2010

Record simple test case in Selenium IDE

In scope of this post I will record a simple test case. It is first draft, first attempt to receive something that we can use in the web testing. Initially I do not suspect to have it represented in human readable format and not repeatable until objects repository is implemented. From first look the test application is nothing else but manage tool, including entities with respective relations. And simple test case can check if a user is able to create new object in the application. Also, it has some kind of authentication. Simple scenario I’m proposing to start with consists of the following steps:
  • Open home page of the application;
  • Log into the application;
  • Open objects list for area entity;
  • Create new item;
  • Log out from the application.
Most of these steps are complex from logic point of view (divided on several elementary actions). For instance login action consists of 3 steps like specify user name, specify user password and click on a button to login. To see objects list first you need to click on item from the main menu, second choose menu item in sub menu. Then objects list is shown. To create new item you have to click on respective button. After that wait for dialog box is opened, then specify meaningful properties, submit changes etc.

The main idea of this scenario is to receive set of steps to work with. So, let’s begin. First, open our test application in FireFox and Selenium IDE. Then, ensure that Selenium IDE is recording (red round button in the top right side is turned on). Then, step by step repeat the scenario above. Afterwards, at the picture below you may see the result I received on my PC:


Let’s review each action in details:

  1. First action from the list is open command. With this command we started from home page of the application;
  2. Then we filled user name value;
  3. Action that fills password value is not recorded for some reason. Let’s postpone with it for awhile. I suggest set low priority for the issues like this and move them to some "outstanding issues" list. And resolve them later;
  4. Next action clicks on a button that makes login operation. It clicks on the object known as imageField;
  5. selectFrame command is nothing else but just wait until dialog is opened. In this dialog all properties for new object to be specified;
  6. Next 3 type commands fill properties for the object (like name, description and other information) in opened dialog box;
  7. Last command from the list clicks Submit button on the dialog box. After that the dialog is closed, new object appeared in the objects list;
  8. At the very end I clicked on Logout button. Unfortunately. It has not been recorded as well.
Eventually, we received first draft of the test case with the scenario above. As you may see this is not human reader and not repeatable as expected from the very beginning:)