2011-04-11

Using Selenium and jQuery for Automated User Testing

I am assuming you are familiar with both jQuery and Selenium. Too, I'll assume you want to use jQuery inside locators or in some way through Selenium to make locators easier using more utilities. So, I'll show you how to do that as well as use your own custom JS files and logic through Selenium just the same by using Selenium.getEval.

First, the below is made possible using the Selenium command or API call addScript. Next, we need a way to take our file or URL based resource and convert it easily into a string which is what addScript is expecting. I use this utility I have:

 public static String inputStream2UTF8(InputStream in) throws IOException {
String ret = null;
BufferedInputStream bin = new BufferedInputStream(in);
InputStreamReader isr = new InputStreamReader(bin, "UTF-8");
StringBuilder sb = new StringBuilder();
int iread = -1;
while ((iread = isr.read()) != -1) {
sb.append((char) iread);
}
ret = sb.toString();
return ret;
}


Next, I like having a more utilitarian method of injecting these resources into the current Selenium session. I use the following two methods together:


/**
* Merges all these different resources into a single input stream, puts them
* into a single JS memory file, and injects this into Selenium using the given
* js element tag ID (jsTagID).
* @param resourceLocator class used to locate resources using cpResourcePaths
* @param cpResourcePaths resource paths relative to resourceLocator or fully qualified
* @param filePaths file system paths, these can be relative if used from a
* running directory, but generally should be full
* @param jsTagID the ID of the script element to inject into Selenium
* @param se the Selenium instance to inject into
*/
public static void injectJavaScriptResourcesTogether(Class resourceLocator,
String[] cpResourcePaths,
String[] filePaths,
String jsTagID,
Selenium se) {
ArrayList ins = new ArrayList();
try {

for(String cpResourcePath : cpResourcePaths){
InputStream in = resourceLocator.getResourceAsStream(cpResourcePath);
if (in != null) {
ins.add(in);
}
}

for(String filePath : filePaths){
File f = new File(filePath).getAbsoluteFile().getCanonicalFile();
InputStream in = new FileInputStream(f);
ins.add(in);
}

SequenceInputStream sin = new SequenceInputStream(Collections.enumeration(ins));

String js = inputStream2UTF8(sin);

//don't swallow here...let the caller do that if
//they need to. API should not eat exceptions generally
se.addScript(js, jsTagID);

} catch (Throwable e) {
if (RuntimeException.class.isInstance(e)) {
throw RuntimeException.class.cast(e);
} else {
throw new RuntimeException(e);
}
} finally {
for (Closeable closer : ins) {
try {
closer.close();
} catch (Throwable e) {
}
}
}
}

public static void injectJavaScriptResource(Class resourceLocator,
String cpResourcePath,
String jsTagID,
Selenium se) {
ArrayList closeables = new ArrayList();
try {
InputStream in = resourceLocator.getResourceAsStream(cpResourcePath);
if (in != null) {
closeables.add(in);
}
String js = inputStream2UTF8(in);

//don't swallow here...let the caller do that if
//they need to. API should not eat exceptions generally
se.addScript(js, jsTagID);

} catch (Throwable e) {
if (RuntimeException.class.isInstance(e)) {
throw RuntimeException.class.cast(e);
} else {
throw new RuntimeException(e);
}
} finally {
for (Closeable closer : closeables) {
try {
closer.close();
} catch (Throwable e) {
}
}
}
}


You can see in the logic above that I attempt to locate the resources given to the methods in different ways. That is pretty straight forward, so I will let the code document itself.

Next, I add JS files into my Java project in NetBeans in a Java package. I will access those things as classpath resources per the code above. I use the following code below from some of my other source code:

public void injectSupportingJavaScript() {
ArrayList closeables = new ArrayList();
try {
String[] resources = new String[]{
"resources/jquery-1.4.4.min.js",
"resources/utils.js",
"resources/nav-utils.js",
"resources/image-dialog-utils.js",
"resources/asset-dialog-utils.js",
"resources/module-utils.js"
};

try {
SEUtilities.injectJavaScriptResourcesTogether(getClass(),
resources,
new String[0],
"ff-aut-javascript",
se);
} catch (Throwable e) {
log.log(Level.WARNING, "Unable to inject required Java Script as a single stream. Will attempt to inject the required .js files individually and continue to run.", e);
try {
for (int i = 0; i < resources.length; i++) {
SEUtilities.injectJavaScriptResource(getClass(), resources[i], "ff-aut-javascript-" + i, se);
}
} catch (Throwable e2) {
StringBuilder emsg = new StringBuilder();
emsg.append("Unable to inject required Java Script as individual streams. ");
emsg.append("Will not be able to continue as the locators can not be used in testing. ");
emsg.append("Selenium may need to be hacked a bit, or ");
emsg.append("the Firefly AUT API .js files need to be further broken up. ");
emsg.append("This is because Selenium HUB has issues with too large of requests. ");
emsg.append("The API already tries to compensate for this, and this measure has failed ");
emsg.append("which usually indicates a .js file injected into Selenium at test time has ");
emsg.append("grown too large in size.");
log.log(Level.SEVERE, emsg.toString(), e);
}
}

} catch (Throwable e) {
if (RuntimeException.class.isInstance(e)) {
throw RuntimeException.class.cast(e);
} else {
throw new RuntimeException(e);
}
} finally {
for (Closeable closer : closeables) {
try {
closer.close();
} catch (Throwable e) {
}
}
}
}

Notice the part about a single resource versus individual in the logic. I do this because there is an issue with Selenium Grid where it seems to use HTTP GET instead of POST in some cases where it should be using POST; at least this is my assumption per the error messages I received. This provides a decent fall back.

In the above, your custom logic will obviously need to inject your own .js files and those will need to be relative to your own class. Once you do that, you can then create a Selenium locater using either pure jQuery inline or you can use your own JS functions to limit the JS logic you have to place in Java files.

Too, using Selenium.getEval(String) you can execute JavaScript directly in the Selenium session. Sometimes you need to do this if your logic depends on some jQuery event listeners and the standard Selenium calls will not activate that logic correctly.

The one caveat to using these things, due to JavaScript targeting, is always pass in window.document to jQuery or your own custom JavaScript functions. The reason is that Selenium will be running in a separate browser window from your application. window.document will point to the window where your applications DOM resides. If you have any question about that specifically let me know.

Some locater example might be:
"dom=your_js_function_doSomething(window.document);"

For getEval it would simply be:
"your_js_function_doSomething(window.document);"

Remeber, the last evaluation or the return statement will be what Selenium returns to the calling logic. Too, you can use the throw statement in your JS to propagate better messages to the calling Java logic.

Enjoy.

1 comment: