Support for 3rd party grids has improved with each release of TestComplete, but in general it is still easier to work with grids based on managed code.
I have been testing a particular application on and off for the last 8 years going right through the TestComplete lineage from v. 2 to 7 without its grids ever being given proper support. By which I mean that viewing the grid in the object browser returns a single window object, rather than the longed-for grid object.
The ability to interact with this grid by specifying row and column parameters seemed like a pipe-dream. Enabling MSAA support brought me some progress, but only in so far as the current cell could be recognised as a text field or combobox, but these components were being re-used for each cell rather there being unique objects for each cell.
I now have a framework I am happy with, allowing me to achieve closure (pun intended) with this unrecognised grid in TestComplete.
The solution involves a mix of some very simple and some quite sophisticated techniques offered by choosing C#Script as the language. I can't promise that the solution is ideal or even any use beyond this particular application but I thought it worth recording nonetheless.
function CommonUI() {
function _process() {
//attach a reference to the AUT process, or launch it if it's not running
var _process = Sys["WaitProcess"]("aut", 5000);
if (!_process["Exists"]) {
TestedApps["aut"]["Run"]();
}
return Sys["WaitProcess"]("aut", 5000);
}
function _window() {
return _process()["WaitWindow"]("Afx:00400000:8*", "*", -1, 5000);
}
...
//public interface to the private methods
this["appProcess"] = function () {
return _process();
}
this["appWindow"] = function () {
return _window();
}
...
}
This has the advantage of allowing us to retrieve a fresh view of an object each time we reference it in the script, rather than one cached in a script variable, which could potentially be out of synch with the application itself.
Note that I have encapsulated the appProcess() and appWindow() methods within a CommonUI object. The technique involves mapping the components of the application to particular testing objects. These objects will inherit some common methods from this CommonUI prototype.
function Grid(_GridName) {
//"that" allows the object to refer both to itself and its prototype chain
var that = this;
var _GridUI;
//Grid will be called as a constructor and will initialise a _GridUI reference to the onscreen grid object
if (!_GridUI) {
Log["Message"]("DEBUG - Finding grid " + _GridName);
_grid()
}
function _grid() {
//note that the ["appWindow"]() method is inherited from the CommonUI prototype
_GridUI = that["appWindow"]()["Window"]("MDIClient", "", 1)["FindChild"]("WndCaption", _GridName, 5)["Window"]("GXWND", "", 1);
_GridUI["Refresh"]();
return _GridUI;
}
...
}
//inherit the CommonUI prototype's methods
Grid.prototype = new CommonUI;
//so in the high-level script, you would refer to a captioned onscreen grid with:
var varGrid = new Grid("Variables");
This allows us to find any grid by its caption. I'm presuming here that the caption is unique, or you would
have to supply addition parameters to the FindChild method (of which more below).
Note that in this AUT, I'm having to go 5 levels deep to find the grid - try to optimise this as far as possible since each
increase in level will be very expensive in terms of test run time.
function Grid(_GridName) {
...
function _gridCell(rowIndex, colName, newValue) {
var colIndex = GridColumns(_GridName, colName);
var thisGridCell = _activateGridCell(rowIndex, colIndex);
if (newValue == undefined) {
return _gridCellValue(colIndex);
}
if (!thisGridCell["Exists"]) {
return "DISABLED";
} else {
newValue = that["setValue"](thisGridCell, colName, newValue);
_GridUI["Keys"]("[Tab]![Tab]");
_GridUI["Refresh"]();
return newValue;
}
}
...
this["cell"] = function (rowIndex, colName, newValue) {
return _gridCell(rowIndex, colName, newValue);
}
...
}
// to return the value in a cell
VarGrid["cell"]("Mortality", "Formula")
//or
VarGrid["cell"](3, 6)
//to set the cell value
VarGrid["cell"]("Mortality", "Formula", "AvgLife + .75")
Here we add a ["cell"]() method to the Grid constructor. Since all parameters are optional in C#Script functions, this method can have different functionality depending on the parameters used.
Note that rows and columns can be referenced by name or index number.
At this point you may be wondering why I'm bothering to write the code as private methods and then expose them as public members of the object. Other than the well-documented advantages of encapsulation, TestComplete rather bizarrely only shows the object's private methods in the Code Explorer, so it is just easier to navigate the scripts this way.
Since we don't have the option to interact directly with a nice grid object, we are limited to doing what the user does.
At this point you may be considering one of the following 3 options:
Please don't - they won't work.
The key to navigating around this grid is using the keyboard. As well as just working, it also has the advantage of firing any key-based events in the application that would be ignored if we were interacting with the underlying objects directly.
function Grid(_GridName) {
var _Row;
var _Col;
...
function _gridRow(rowIndex) {
//escape from editing the active cell and return to column 0
_GridUI["Keys"]("[Tab]![Tab][Home]");
if (rowIndex == undefined) {
return _Row;
}
if (_Row != rowIndex){
//reset to first cell and activate it
_GridUI["Keys"]("^[Home]");
_GridUI["Keys"]("[F2]");
if (BuiltIn["VarType"](rowIndex) == varOleStr) {//row indexed on variable name
_GridUI["Keys"]("^[Down]");
var FoundVar = _GridUI["Window"]("GXEDIT", "", 1)["wText"];
while (FoundVar != rowIndex) {
_GridUI["Keys"]("[Up]");
FoundVar = _GridUI["Window"]("GXEDIT", "", 1)["wText"];
}
} else {//row indexed by number or -1 for new
if (rowIndex < 0) {
_GridUI["Keys"]("^[Down]");
} else {
if (rowIndex > 0) {
for (var i = 0; i < rowIndex; i++) {
_GridUI["Keys"]("[Down]");
}
}
}
}
}
//go back to first column
_GridUI["Keys"]("[Tab]^[Left]");
_GridUI["Refresh"]();
_Row = rowIndex;
return _Row;
}
function _activateGridCell(rowIndex, colIndex) {
_gridRow(rowIndex);
//if colIndex is a name, rather than a number, convert it but store the name
if (BuiltIn["VarType"](colIndex) == varOleStr) {
_Col = colIndex;
colIndex = GridColumns(_GridName, colIndex);
}
if (colIndex > 0) {
for (var i = 0; i < colIndex; i++) {
_GridUI["Keys"]("[Tab]");
}
}
_GridUI["Keys"]("[F2]");
_GridUI["Refresh"]();
//look for the only visible GXEDIT or GXCOMBOBOX in the grid
var fProperties = Sys["OleObject"]("Scripting.Dictionary");
fProperties["Add"](0, "WndClass");
fProperties["Add"](1, "VisibleOnScreen");
var fValues = Sys["OleObject"]("Scripting.Dictionary");
fValues["Add"](0, "GX*");
fValues["Add"](1, true);
return _GridUI["FindChild"](fProperties["Items"](), fValues["Items"]());
}
...
this["row"] = function (rowIndex) {
return _gridRow(rowIndex);
}
this["activateCell"] = function (rowIndex, colIndex) {
return _activateGridCell(rowIndex, colIndex);
}
...
}
The ["row"]() method checks the private _Row property to see if we are already on the requested row and this in turn is used by the ["activateCell"]() method to navigate to a particular cell and activate it for editing.
In this AUT, the F2 and Tab keys are used to activate or deactivate a cell, respectively. Note that each new entry in the grid is made in the final row, so when looking for a particular row by name (i.e. the value in column 0), we move to the last row and then move up, checking the value in the cell till we get a match.
As mentioned in the introduction, this AUT only exposes a single visible GXEDIT or GXCOMBOBOX for the active cell, so this method returns either of those object types as the active cell.
function CommonUI() {
...
function _setValue(stField, stName, stValue) {
//set the value of a specified Field object in a grid or dialog window
//the field caption is required to produce sensible messages
var stClass = stField["WndClass"];
//any kind of edit field
if ((stClass == "Edit")||(stClass == "GXEDIT")||(stClass == "RichEdit20W")) {
stField["Keys"]("[Home]![End]" + stValue);
} else { //assuming combobox
stField["ClickItem"](stValue);
}
//check the value has been set
if (stField["wText"] == stValue) {
Log["Message"](stName + " value set correctly")
} else {
Log["Error"]("Problem setting " + stName + " value");
}
return stField["wText"];
}
...
this["setValue"] = function (stField, stName, stValue) {
_setValue(stField, stName, stValue);
}
...
}
A ["setValue"]() method is defined in CommonUI as it is equally applicable to editing values in dialog boxes as well as grids, etc. The ["cell"]() method defined earlier in the Grid constructor relies on inheriting this method for editing cell values.
If we are dealing with an edit field, overwrite it with the new value. Here we could set the field object's value directly but I choose not to in case any key-based events need to be fired in the AUT. A combobox value can be set using ClickItem() since this is an accurate reproduction of user input.
It's a good idea to check for errors in setting values at this level to avoid having to pepper the high-level script with error-handling conditions.
We cannot (or choose not to) interact with the grid's cell objects directly, so reading values could be achieved by navigating to a cell to activate its edit field and then reading the value. This is fine unless, as in this case, some fields can be disabled and have no corresponding edit field object. The solution is brilliantly simple:
function Grid(_GridName) {
...
function _gridCellValue(colIndex) {
//copy entire row to clipboard
_GridUI["Keys"]("[Tab]![Tab][Home]!^[Right]^c");
//split by tabs into an array and get the member in the chosen column
var thisValue = Sys.Clipboard["split"](chr(9))[colIndex];
//clear the selection back to column 0
_GridUI["Keys"]("[Down][Up]^[Left]");
//clean up unwanted characters
thisValue = thisValue["replace"](/[\n\r]/g, " ");
thisValue = thisValue["replace"](/\x22/g, "");
thisValue = thisValue["replace"](/; /g, ";");
thisValue = thisValue["replace"](/ = /g, "=");
thisValue = thisValue["replace"](/ /g, "");
return thisValue;
}
...
}
This particular application allows you to copy grids (or any part of them) to the clipboard, so we use that feature to copy a row and return an array containing all the values in that row. Given that we know the index of the column we are interested in (see below), we can pull it out of the array:
Note that there is no public interface to this method, since it is handled via the ["cell"]() method.
var GridColumns = (function () {
var _gridCols;
if (!_gridCols) {
Log["Message"]("DEBUG - Initialising Columns", "", 100);
//define an associative array (object)
//with each property an array of column names
_gridCols = {
Data_view_variables: [
"Variable",
"Data type",
"Aggregates",
"Portfolio",
...
"Size",
"Size on record",
"Start position"
]
Assumption_set_variables: [
"Variable",
"Formula",
...
"Category"
]
Variables: [
"Variable",
...
"Portfolio",
"Display format"
]
Sub_assumption_sets: [
"Sub assumption set",
"Description"
]
}
}
return function (gridName, colName) {
//translate the grid name to a gridCols property name
var _gridName = gridName["replace"](/ /g, "_");
if (_gridCols[_gridName]) {
return _gridCols[_gridName]["indexOf"](colName);
} else {
Log["Error"]("DEBUG - Grid '" + gridName + "' has not been defined");
return -1;
}
}
}());
//the indexOf() method does not exist natively, but we can augment the Array prototype with:
Array.prototype.indexOf = function (colIndex) {
for (var i = 0, l = this.length; i < l; i++) {
if (this[i] == colIndex) {
return i;
}
}
return -1;
}
//The brute force indexOf() approach is 100x faster than using something like dotnet.ArrayList.IndexOf()
It really is worth getting your head around the idea of closures
as they are extremely useful.
GridColumns is a variable, but its value is a function that returns the index for a named column in a named grid. The brilliant part is that implementing the underlying arrays as private variables within a closure, we don't have to either declare the arrays as global variables and remember to initialise them at the start of every test run or have them as variables that are (wastefully) initialised on every call to GridColumns().
The _gridCols mapping is initialised automatically once and only once the first time you call GridColumns(). The syntax might be a bit obscure (the braces in the last line are particularly important) but it's a powerful technique.
The brute force technique of indexOf() might look a bit clumsy but I had considered an alternative such as using a dot net ArrayList, which has its own indexOf() method, only to find that the latter ran one hundred times slower!
function Grid(_GridName) {
...
function _isDisabled(rowIndex, colName) {
var thisCell = _activateGridCell(rowIndex, colName);
if (!thisCell["Exists"]) {
return true;
}
if ((thisCell["WndClass"] == "GXCOMBOBOX") && (thisCell["wItemCount"] == 1)) {
return true;
}
return false;
}
...
this["isDisabled"] = function (rowIndex, colName) {
return _isDisabled(rowIndex, colName);
}
...
}
In this AUT, a combobox is also considered disabled if it offers only one option.
The basic interactions with the grid are now sorted, but you don't really want to write a high level script that edits and checks each cell individually, so use the framework to supply some intermediate methods:
function Grid(_GridName) {
...
function _newVariable(ValuesArray) {
var i = 0;
var gVar = ValuesArray[i];
i++;
var gValue = ValuesArray[i];
_gridCell(-1, gVar, gValue);
_Row = gValue;
i++;
while (ValuesArray[i] != undefined) {
gVar = ValuesArray[i];
i++;
gValue = ValuesArray[i];
_gridCell(_Row, gVar, gValue);
i++;
}
}
...
function _checkDefaultValues(checkRow, CheckArray) {
var CheckErrors = 0;
var i = 0;
while (CheckArray[i] != undefined) {
var CheckField = CheckArray[i];
i++;
var CheckValue = CheckArray[i];
if (CheckField["substring"](0,1) == "@") {
CheckField = CheckField["slice"](1);
if (!_isDisabled(checkRow, CheckField)) {
CheckErrors++;
Log["Error"](CheckField + " should be disabled");
}
} else {
if (_isDisabled(checkRow, CheckField)) {
CheckErrors++;
Log["Error"](CheckField + " should not be disabled");
}
}
var FoundValue = _gridCell(checkRow, CheckField);
if (FoundValue != CheckValue) {
CheckErrors++;
Log["Error"](CheckField + " default value wrong");
Log["Warning"]("Found:" + FoundValue);
Log["Warning"]("Expected:" + CheckValue);
}
i++;
}
if (CheckErrors == 0) {
Log["Message"]("Default Values all OK");
}
}
...
this["newVariable"] = function (ValuesArray) {
return _newVariable(ValuesArray);
}
this["checkDefaultValues"] = function (checkRow, CheckArray) {
return _checkDefaultValues(checkRow, CheckArray);
}
...
}
//["newVariable"]() takes an array of column, value pairs and enters them into a new row, e.g.
var AssSetVars = new Grid("Assumption set variables");
AssSetVars["newVariable"](
["Variable", "AS_Var_1",
"Data type", "Character",
"Formula", "'ABC'"]);
//["checkDefaultValues"]() takes a row name and array of column, expected value pairs
//a leading @ indicates that the field should be disabled
//flags all errors or reports that all checked fields are OK, e.g.
AssSetVars["checkDefaultValues"]("AS_Var_1",
["Portfolio", "No",
"@Aggregates", "No",
"@Display format", "None"]);