『Selenium 2.XをJavaScriptで使う』でJavaScriptでもWebDriverのコーディングができることがわかりましたが、どうも釈然としません。その原因の1つはドキュメントとの親和性でした。テストの実施前に記述する仕様書、テストの結果を示す報告書が必要です。プログラミング言語はこういったものと連携するのが上手ではありませんが、XMLならばなんとかなりそうな気がします。
そこで、XMLで定義したテストケースでWebDriverを実行するXmlDriverなるものを考えてみました。Driverの名前につけるのは、本来ならばブラウザの名前なのでXmlDriverという言い方は間違っているかもしれませんが、プロトタイプだからあまり深く考えないことにします。
<?xml version="1.0" encoding="UTF-8"?>
<suite name="Web Unit Test">
<driver name="firefox" version="11.0" capabilities=""/>
<test name="トップ">
<get>http://shorindo.com</get>
<waitfor timeout="5"/>
<verifyPresent>
<text value="松林堂"/>
<element link="HOME"/>
<element link="会社案内"/>
<element link="業務実績"/>
<element link="研究開発"/>
</verifyPresent>
</test>
<test name="会社案内" depends="トップ">
<click link="会社案内"/>
<waitfor timeout="5"/>
<verifyPresent>
<text value="会社名"/>
<text value="所在地"/>
<text value="資本金"/>
</verifyPresent>
</test>
<test name="業務実績" depends="会社案内">
<click link="業務実績"/>
<waitfor timeout="5"/>
<verifyPresent>
<element xpath="//h1/a[text()='業務実績']"/>
<element xpath="//h2/a[text()='2011年度']"/>
<element xpath="//h2/a[text()='2010年度x']"/>
</verifyPresent>
</test>
</suite>
importPackage(java.io);
var xmldriver = { countTests:0, countVerify:0 };
xmldriver.trace = function() {
print(arguments.callee.caller);
};
xmldriver.load = function(fileName) {
importPackage(javax.xml.parsers);
importPackage(javax.xml.xpath);
var xmlFactory = DocumentBuilderFactory.newInstance();
var builder = xmlFactory.newDocumentBuilder();
this.document = builder.parse(fileName);
var xpathFactory = XPathFactory.newInstance();
this.xpath = xpathFactory.newXPath();
};
xmldriver.queryForNode = function(query, node) {
var target;
if (typeof(node) == 'object') target = node;
else target = this.document;
var expr = this.xpath.compile(query);
return expr.evaluate(target, XPathConstants.NODE);
};
xmldriver.queryForList = function(query, node) {
var target;
if (typeof(node) == 'object') target = node;
else target = this.document;
var expr = this.xpath.compile(query);
return expr.evaluate(target, XPathConstants.NODESET);
};
xmldriver.info = function() {
var tests = this.queryForList('/suite/test');
print("Number of tests=" + tests.length);
//var verifies = this.queryForList('//verify');
//print("verify=" + verifies.length);
};
xmldriver.findElements = function(attrs) {
//print("findElements(" + attrs + ")");
var result;
for (var i = 0; attrs && i < attrs.length; i++) {
var attr = attrs.item(i);
var name = attr.getName();
var value = attr.getValue();
//print("findElements(" + name + "," + value + ")");
if (name == 'id') {
result = this.driver.findElementsById(value);
if (result) break;
}
else if (name == 'name') {
result = this.driver.findElementsByName(value);
if (result) break;
}
else if (name == 'tag') {
result = this.driver.findElementsByTagName(value);
if (result) break;
}
else if (name == 'class') {
result = this.driver.findElementsByClassName(value);
if (result) break;
}
else if (name == 'selector') {
result = this.driver.findElementsByCssSelector(value);
if (result) break;
}
else if (name == 'xpath') {
result = this.driver.findElementsByXPath(value);
if (result) break;
}
else if (name == 'link') {
result = this.driver.findElementsByLinkText(value);
if (result) break;
}
}
return result;
};
xmldriver.findElement = function(attrs) {
//print("findElement(" + attrs + ")");
var result = this.findElements(attrs);
if (result && result.size() > 0) return result.get(0);
};
xmldriver.getAttrsString = function(attrs) {
//print("getAttrsString(" + attrs + ")");
if (attrs == null) return;
var result = '';
var sep = '';
for (var i = 0; i < attrs.length; i++) {
result += sep + attrs.item(i).getName() + '=' + attrs.item(i).getValue();
sep = ',';
}
return result;
};
xmldriver.dump = function(node) {
if (node.nodeName == '#text') {
var t = node.textContent + ''; //avoid java string.
if (!t) return '';
t = t.replace(/\s+/mg, '');
if (!t) return '';
else return t;
}
var result = '<' + node.nodeName;
var attrs = node.attributes;
for (var i = 0; attrs && i < attrs.length; i++) {
var attr = attrs.item(i);
result += ' ' + attr.getName() + '="' + attr.getValue() + '"';
}
if (node.hasChildNodes()) {
result += '>';
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes.item(i);
result += this.dump(child);
}
result += '</' + node.nodeName + '>';
} else {
result += '/>';
}
return result;
};
xmldriver.run = function() {
this.doSuite(this.document.documentElement);
};
xmldriver.quit = function() {
if (this.driver) this.driver.quit();
}
/*
* XML nodes handling.
*/
xmldriver.doSuite = function(node) {
if (node.nodeName != 'suite')
throw new Error('invalid format');
//print(node.nodeName);
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes.item(i);
if (child.nodeName == 'driver') {
this.doDriver(child);
}
else if (child.nodeName == 'test') {
this.doTest(child);
}
}
};
xmldriver.doDriver = function(node) {
//print(node.nodeName);
var driverName = node.getAttribute('name');
if (driverName == 'firefox') {
importPackage(org.openqa.selenium);
importPackage(org.openqa.selenium.firefox);
this.driver = new FirefoxDriver();
}
else if (driverName == 'ie') {
importPackage(org.openqa.selenium);
importPackage(org.openqa.selenium.ie);
this.driver = new InternetExplorerDriver();
}
else if (driverName == 'htmlunit') {
importPackage(org.openqa.selenium);
importPackage(org.openqa.selenium.htmlunit);
this.driver = new HtmlUnitDriver(true);
}
else {
throw new Error('valid driver name not specified!');
}
this.driver.manage().timeouts().implicitlyWait(10, java.util.concurrent.TimeUnit.SECONDS);
};
xmldriver.doTest = function(node) {
//print(node.nodeName);
print((++this.countTests) + ". " + node.getAttribute('name'));
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes.item(i);
if (child.nodeName == 'verify') {
this.doVerify(child);
}
else if (child.nodeName == 'get') {
this.doGet(child);
}
else if (child.nodeName == 'click') {
this.doClick(child);
}
else if (child.nodeName == 'sendkeys') {
this.doSendKeys(child);
}
else if (child.nodeName == 'waitfor') {
this.doWaitfor(child);
}
else if (child.nodeName == 'frame') {
this.doFrame(child);
}
else if (child.nodeName == 'window') {
this.doWindow(child);
}
else if (child.nodeName == 'echo') {
this.doEcho(child);
}
else if (child.nodeName == 'snapshot') {
this.doSnapShot(child);
}
}
};
xmldriver.doGet = function(node) {
this.driver.get(node.textContent);
};
xmldriver.doClick = function(node) {
var element = this.findElement(node.attributes);
if (element) element.click();
else throw new Error("element[" + this.getAttrsString(node.attributes) + "] not found.");
};
xmldriver.doSendKeys = function(node) {
//print("doSendKeys(" + this.getAttrsString(node.attributes) + ")");
var element = this.findElement(node.attributes);
if (element) element.sendKeys(node.getAttribute("value"));
else throw new Error("element[" + this.getAttrsString(node.attributes) + "] not found.");
};
xmldriver.doFrame = function(node) {
this.driver.switchTo().frame(node.getAttribute('name'));
try {
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes.item(i);
if (child.nodeName == 'verify') {
this.doVerify(child);
}
else if (child.nodeName == 'get') {
this.doGet(child);
}
else if (child.nodeName == 'click') {
this.doClick(child);
}
else if (child.nodeName == 'sendkeys') {
this.doSendKeys(child);
}
else if (child.nodeName == 'waitfor') {
this.doWaitfor(child);
}
else if (child.nodeName == 'echo') {
this.doEcho(child);
}
}
} catch (e) {
print(e);
}
this.driver.switchTo().defaultContent();
};
xmldriver.doWindow = function(node) {
this.driver.switchTo().window(node.getAttribute('name'));
try {
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes.item(i);
if (child.nodeName == 'verify') {
this.doVerify(child);
}
else if (child.nodeName == 'get') {
this.doGet(child);
}
else if (child.nodeName == 'click') {
this.doClick(child);
}
else if (child.nodeName == 'sendkeys') {
this.doSendKeys(child);
}
else if (child.nodeName == 'waitfor') {
this.doWaitfor(child);
}
else if (child.nodeName == 'echo') {
this.doEcho(child);
}
}
} catch (e) {
print(e);
}
this.driver.switchTo().defaultContent();
};
xmldriver.doEcho = function(node) {
print("[echo] " + node.textContent);
};
xmldriver.doSnapShot = function(node) {
/*
var bytes = this.driver.getScreenshotAs(OutputType.BYTES);
var fos = new FileOutputStream("img/screenshot" + (new Date()).getTime() + ".png");
fos.write(bytes, 0, bytes.length);
fos.close();
*/
};
xmldriver.doWaitfor = function(node) {
};
xmldriver.doVerify = function(node) {
//print("doVerify()");
var result = true;
var count = 1;
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes.item(i);
var r = false;
try {
if (child.nodeName == 'title') {
r = this.doTitle(child);
}
else if (child.nodeName == 'text') {
r = this.doText(child);
}
else if (child.nodeName == 'element') {
r = this.doElement(child);
}
else if (child.nodeName == 'enabled') {
r = this.doEnabled(child);
}
else {
continue;
}
} catch (e) {
print(e);
}
if (r) {
print("verify success[" + (this.countVerify++) + "] " + this.dump(child));
} else {
print("verify failure[" + (this.countVerify++) + "] " + this.dump(child));
result = false;
}
}
return result;
};
xmldriver.doTitle = function(node) {
var title = this.driver.getTitle();
//print("doTitle(" + node.getAttribute('value') + ")<=>" + title);
if (title == node.getAttribute('value')) return true;
else return false;
};
xmldriver.doElement = function(node) {
var el = this.findElement(node.attributes);
var result = node.getAttribute('present');
var displayed = node.getAttribute('displayed');
var enabled = node.getAttribute('enabled');
var selected = node.getAttribute('selected');
if (result == 'false') result = false;
else result = true;
//print("doElement(" + node.nodeName + ")=>" + el);
if (el) {
//print("element found:" + this.dump(node));
if (displayed == 'true') result = el.isDisplayed();
else if (displayed == 'false') result = !el.isDisplayed();
if (enabled == 'true') result = el.isEnabled();
else if (enabled == 'false') result = !el.isEnabled();
if (selected == 'true') result = el.isSelected();
else if (selected == 'false') result = !el.isSelected();
return result;
} else {
//print("element not found:" + this.dump(node));
return !result;
}
};
xmldriver.doText = function(node) {
var text = node.getAttribute('value');
var el = this.driver.findElementByXPath('//*[contains(text(),"' + text + '")]');
var result = node.getAttribute('present');
if (result == 'false') result = false;
else result = true;
if (el) {
return result;
} else {
return !result;
}
};
if (arguments.length > 0) {
for (var i = 0; i < arguments.length; i++) {
xmldriver.load(arguments[i]);
xmldriver.run();
}
xmldriver.quit();
} else {
print("usage: xmldriver.js suite1.xml suite2.xml ..");
}
JavaVMの第一引数にRhinoのshell、第二引数にxmldriver.jsプログラム本体、第三引数にテストケースのXMLファイルを指定します。
% java -cp "js.jar;selenium-server-standalone-2.20.0.jar" \
org.mozilla.javascript.tools.shell.Main \
xmldriver.js \
suites.xml 2> xmldriver.log