Use of script language closures

All supported script languages in the automated GUI Testing Tool Squish support closures. In this blog I'll write up a simple example how closures can be used to unify function calls that seem to have different set of arguments.

Assume a test snippet that tests two ways to open a settings dialog, together with two ways to cancel the dialog. The script snippet may look like this.
[code language="javascript" title="Dialog open/close recording"]function main() {
...
activateItem(waitForObjectItem(":MenuBar", "File"));
activateItem(waitForObjectItem(":File_Menu", "Settings"));
type(waitForObject(":Settings_Dialog"), "<Esc>");

type(waitForObject(":MainWindow"), "<Ctrl+Shift+p>");
clickButton(waitForObject(":Settings.Cancel_Button"));
...
}
[/code]
To harden this script, e.g. test the dialog was not open before opening, is really closed after closing, etc, the following refactoring is done.
[code language="javascript" title="Dialog open/close functions"]function openPreferenceDialog(how)
{
try {
findObject(":Settings_Dialog");
test.fail("Dialog is open");
return;
} catch (e) {}

if (how == "menu") {
activateItem(waitForObjectItem(":MenuBar", "File"));
activateItem(waitForObjectItem(":File_Menu", "Settings"));
} else if (how == "shortcut") {
type(waitForObject(":MainWindow"), "<Ctrl+Shift+p>");
}
waitForObject(":Settings_Dialog");
}

function closePreferenceDialog(how)
{
findObject(":Settings_Dialog");

if (how == "cancelButton") {
clickButton(waitForObject(":Settings.Cancel_Button"));
} else if (how == "esc") {
type(waitForObject(":Settings_Dialog"), "<Esc>");
}
for (var i = 0; i < 10; ++i) {
try {
findObject(":Settings_Dialog");
snooze(.5);
} catch (e) {
break; // good, not found anymore
}
}
}

function main() {
...
openPreferenceDialog("menu");
closePreferenceDialog("esc");

openPreferenceDialog("shortcut");
closePreferenceDialog("cancelButton");
...
}
[/code]
The how argument selects between the two open/close methods.
The openPreferenceDialog/closePreferenceDialog functions hard-codes the objects it targets. Therefore refactor it further so that the how string argument is a function instead. So something like this.
[code language="javascript" title="Dialog open function"]function openDialog(dialogName, openFunction)
{
try {
findObject(dialogName);
test.fail("Dialog is open");
return;
} catch (e) {}

openFunction();

waitForObject(dialogName);
}

function main()
{
...
openDialog(":Settings_Dialog", function() {
activateItem(waitForObjectItem(":MenuBar", "File"));
activateItem(waitForObjectItem(":File_Menu", "Settings"));
});
...
[/code]
B.t.w. for this particular example, I wouldn't further generalize the script code in practice.

But for the purpose of this blog, using the scripts closure feature, lets make these openFunction/closeFunction functions re-usable.
Somehow the object names must be passed to these functions. And intuitively pass these names to openDialog which then pass them further to openFunction. Both activateItem and type take an object name and a second item or text argument but, for closing, clickButton and type don't match in number of arguments.
Here closures provide a rather simple solution. Use a function that provides a function. The returned function can use the arguments passed to outer-function1.
[code language="javascript" title="Dialog open/close generalization"]function typeFunction(objectName, text) {
return function() {
type(waitForObject(objectName), text);
};
}

function clickMenuFunction(menuBar, menuBarItem, menu, item) {
return function() {
activateItem(waitForObjectItem(menuBar, menuBarItem));
activateItem(waitForObjectItem(menu, item));
}
}

function clickButtonFunction(objectName) {
return function() {
clickButton(waitForObject(objectName));
};
}

function openDialog(dialogName, openFunction)
{
try {
findObject(dialogName);
test.fail("Dialog is open");
return;
} catch (e) {}
openFunction();
waitForObject(dialogName);
}

function closeDialog(dialogName, closeFunction)
{
findObject(dialogName);
closeFunction()
for (var i = 0; i < 10; ++i) {
try {
findObject(dialogName);
snooze(.5);
} catch (e) {
break; //good, not found anymore
}
}
}

function main() {
...
openDialog(":Settings_Dialog", clickMenuFunction(":MenuBar", "File", ":File_Menu", "Settings"));
closeDialog(":Settings_Dialog", typeFunction(":Settings_Dialog", "<Esc>"));
openDialog(":Settings_Dialog", typeFunction(":MainWindow", "<Ctrl+Shift+p>"));
closeDialog(":Settings_Dialog", clickButtonFunction(":Settings.Cancel_Button"));
...
}
[/code]
Closures are often used for creating so called callback functions to prevent the need for global variables. The Squish API has waitFor and all the Squish editions have an installEventHandler function, which can be called with a callback function.

Closure reference

Closures in Perl
Closures in Python
Closures in Ruby
Closures in TCL
WikiPedia Closure page

Comments