The Squish script API features a useful function called waitFor; what's less known is the fact that this function not only supports being called with a script statement but also with an arbitrary callable value.
Consider an application which performs a long-running operation and eventually displays the results of that operation. This could be an application downloading a large file from the Internet or a system administration tool enumerating all files on the hard disk. Automating such an application typically requires that after triggering the operation (e.g. by clicking a button), the test script waits for the operation to complete before verifying that the result is displayed as expected.
It's not uncommon that applications show some sort of progress indicator to provide user feedback as the operation is being performed. Progress bars are commonly used to implement this feedback but many user interfaces visualize activity by showing an animation of some sort which provides no hint as to how far the operation progressed. The sheer lack of this animation however does not necessarily indicate that the operation finished.
For example, a download operation may cause an animation to be displayed and a temporary file might be created on disk which holds the downloaded data. When the download finished, the animation is stopped but the application is still busy processing the downloaded data before removing the temporary file. In such cases, it's insufficient to only synchronize based on the user interface - the test script also needs to consider the existence of the temporary file to decide whether the operation finished.
A Naive Solution
A naive attempt to implement this form of synchronization may be to implement a basic loop which terminates once a temporary file disappeared:
mouseClick(":Start Download_Button")
tempFileName = "..." # Name of the temporary file created during the download
while os.path.exists(tempFileName):
snooze(1)
This suffers from various issues, the biggest one being that the loop runs forever if the temporary file is not deleted. It's imaginable that the application crashes and hence the file does not get deleted. Or the download gets stuck. Or the application code has a defect due to which the file is not deleted even though the download finished. To handle these error scenarios, the waitFor is very handy.
Handling Timeouts Using waitFor
The waitFor function expects a script expression which should return either true or false, depending on whether the condition to wait for has been met or not. The documentation explains:
This function is designed as a quick and easy way to poll for a condition in the AUT, where each
condition
execution takes a very short time (typically a fraction of a second), and always returnstrue
orfalse
. This is useful if you want to synchronize your script execution to a certain state in the AUT.
It allows us to rewrite our earlier example as follows:
mouseClick(":Start Download_Button")
tempFileName = "..." # Name of the temporary file created during the download
waitFor("os.path.exists(tempFileName")
If the temporary file does not exist (i.e. os.path.exists yields False), the function will wait for a short moment and then check again. It will repeat doing this until the script expression returns True. Unless specified, this way of invoking waitFor will wait forever.
However, if you don't want the test script to wait forever, a timeout value can be specified using an optional second argument which defines the number of milliseconds to wait. For instance, if you expect that the operation can take longer, you may want to wait for 60 seconds by using:
mouseClick(":Start Download_Button")
tempFileName = "..." # Name of the temporary file created during the download
waitFor("os.path.exists(tempFileName", 60 * 1000)
In case the timeout expires, the waitFor function returns False and the test script can handle this error indication (e.g. by logging a descriptive user message using the test.fatal function).
Improved Usage Of waitFor Through Callables
Did you notice the mistake we did further up? Here's the last script snippet again:
mouseClick(":Start Download_Button")
tempFileName = "..." # Name of the temporary file created during the download
waitFor("os.path.exists(tempFileName", 60 * 1000)
We accidentally mistyped the script expression passed to the waitFor function, missing the closing )! This may well have gone unnoticed until the test is executed because the script expression is passed as a plain string - the script editor (e.g. the Squish IDE) has no knowledge that this string is meant to be valid script code.
This can be avoided by taking advantage of a lesser-known feature of waitFor. It can be called not only with a plain string but a real callable value. In the case of Python, a lambda expression can be used:
mouseClick(":Start Download_Button")
tempFileName = "..." # Name of the temporary file created during the download
waitFor(lambda: os.path.exists(tempFileName), 60 * 1000)
Other languages use different ways to express the same concept. For example, in
JavaScript an anonymous function could be passed:
mouseClick(":Start Download_Button");
var tempFileName = "..."; // Name of the temporary file created
waitFor(function() { return File.exists(tempFileName) }, 60 * 1000);
In Ruby, a proc/lambda/block can be passed, just like in Perl. Of course, in all cases you can also pass a plain function.
By passing a callable value, your test script code becomes
- More efficient since no string argument needs to be parsed.
- Easier to read due to proper syntax highlighting in script code editors.
- Easier to debug since syntax errors are clearly marked as such in the
Squish IDE instead of resulting in script exceptions at runtime.