文章目錄
製作 Selenium IDE 的 xUnit.net 2.0 版 Formatter
TDD 課程中,91 大介紹了 Selenium IDE 的用法,我的心得筆記請參考 使用 Selenium IDE 與 C# 做 Web UI 測試,因為 Selenium IDE 預設只支援 Nunit,所以 91 大動手做了個 MSTest 的版本,詳細內容請參考 Export to C#/WebDriver/MSTest,當下就想到好像每次 xUnit 都被忽略,於是就興起自己做 xUnit Selenium IDE Formatter 的念頭,就來看看要怎麼修改吧
修改 formatter
細節就不多提,有興趣的人可以仔細研究,大意就是引用 xUnit 的 namespace 並使用 xUnit 的寫法來調整 ,程式碼在如下,也放上 GitHub:C-sharp-xUnit.net-2.0-WebDriver
/*
* Formatter for Selenium 2 / WebDriver .NET (C#) client.
*/
if (!this.formatterType) { // this.formatterType is defined for the new Formatter system
// This method (the if block) of loading the formatter type is deprecated.
// For new formatters, simply specify the type in the addPluginProvidedFormatter() and omit this
// if block in your formatter.
var subScriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService(Components.interfaces.mozIJSSubScriptLoader);
subScriptLoader.loadSubScript('chrome://selenium-ide/content/formats/webdriver.js', this);
}
function testClassName(testName) {
return testName.split(/[^0-9A-Za-z]+/).map(
function(x) {
return capitalize(x);
}).join('');
}
function testMethodName(testName) {
return "The" + capitalize(testName) + "Test";
}
function nonBreakingSpace() {
return "\"\\u00a0\"";
}
function array(value) {
var str = 'new String[] {';
for (var i = 0; i < value.length; i++) {
str += string(value[i]);
if (i < value.length - 1) str += ", ";
}
str += '}';
return str;
}
Equals.prototype.toString = function() {
return this.e1.toString() + " == " + this.e2.toString();
};
Equals.prototype.assert = function() {
return "Assert.Equal(" + this.e1.toString() + ", " + this.e2.toString() + ");";
};
Equals.prototype.verify = function() {
return verify(this.assert());
};
NotEquals.prototype.toString = function() {
return this.e1.toString() + " != " + this.e2.toString();
};
NotEquals.prototype.assert = function() {
return "Assert.NotEqual(" + this.e1.toString() + ", " + this.e2.toString() + ");";
};
NotEquals.prototype.verify = function() {
return verify(this.assert());
};
function joinExpression(expression) {
return "String.Join(\",\", " + expression.toString() + ")";
}
function statement(expression) {
return expression.toString() + ';';
}
function assignToVariable(type, variable, expression) {
return capitalize(type) + " " + variable + " = " + expression.toString();
}
function ifCondition(expression, callback) {
return "if (" + expression.toString() + ")\n{\n" + callback() + "}";
}
function assertTrue(expression) {
return "Assert.True(" + expression.toString() + ");";
}
function assertFalse(expression) {
return "Assert.False(" + expression.toString() + ");";
}
function verify(statement) {
return statement ;
}
function verifyTrue(expression) {
return verify(assertTrue(expression));
}
function verifyFalse(expression) {
return verify(assertFalse(expression));
}
RegexpMatch.patternToString = function(pattern) {
if (pattern != null) {
//value = value.replace(/^\s+/, '');
//value = value.replace(/\s+$/, '');
pattern = pattern.replace(/\\/g, '\\\\');
pattern = pattern.replace(/\"/g, '\\"');
pattern = pattern.replace(/\r/g, '\\r');
pattern = pattern.replace(/\n/g, '(\\n|\\r\\n)');
return '"' + pattern + '"';
} else {
return '""';
}
};
RegexpMatch.prototype.toString = function() {
return "Regex.IsMatch(" + this.expression + ", " + RegexpMatch.patternToString(this.pattern) + ")";
};
function waitFor(expression) {
return "for (int second = 0;; second++) {\n" +
indents(1) + 'if (second >= 60) Assert.Fail("timeout");\n' +
indents(1) + "try\n" +
indents(1) + "{\n" +
(expression.setup ? indents(2) + expression.setup() + "\n" : "") +
indents(2) + "if (" + expression.toString() + ") break;\n" +
indents(1) + "}\n" +
indents(1) + "catch (Exception)\n" +
indents(1) + "{}\n" +
indents(1) + "Thread.Sleep(1000);\n" +
"}";
}
function assertOrVerifyFailure(line, isAssert) {
var message = '"expected failure"';
var failStatement = isAssert ? "Assert.Fail(" + message + ");" :
"verificationErrors.Append(" + message + ");";
return "try\n" +
"{\n" +
line + "\n" +
failStatement + "\n" +
"}\n" +
"catch (Exception) {}\n";
}
function pause(milliseconds) {
return "Thread.Sleep(" + parseInt(milliseconds, 10) + ");";
}
function echo(message) {
return "Console.WriteLine(" + xlateArgument(message) + ");";
}
function formatComment(comment) {
return comment.comment.replace(/.+/mg, function(str) {
return "// " + str;
});
}
function keyVariable(key) {
return "Keys." + key;
}
this.sendKeysMaping = {
BKSP: "Backspace",
BACKSPACE: "Backspace",
TAB: "Tab",
ENTER: "Enter",
SHIFT: "Shift",
CONTROL: "Control",
CTRL: "Control",
ALT: "Alt",
PAUSE: "Pause",
ESCAPE: "Escape",
ESC: "Escape",
SPACE: "Space",
PAGE_UP: "PageUp",
PGUP: "PageUp",
PAGE_DOWN: "PageDown",
PGDN: "PageDown",
END: "End",
HOME: "Home",
LEFT: "Left",
UP: "Up",
RIGHT: "Right",
DOWN: "Down",
INSERT: "Insert",
INS: "Insert",
DELETE: "Delete",
DEL: "Delete",
SEMICOLON: "Semicolon",
EQUALS: "Equal",
NUMPAD0: "NumberPad0",
N0: "NumberPad0",
NUMPAD1: "NumberPad1",
N1: "NumberPad1",
NUMPAD2: "NumberPad2",
N2: "NumberPad2",
NUMPAD3: "NumberPad3",
N3: "NumberPad3",
NUMPAD4: "NumberPad4",
N4: "NumberPad4",
NUMPAD5: "NumberPad5",
N5: "NumberPad5",
NUMPAD6: "NumberPad6",
N6: "NumberPad6",
NUMPAD7: "NumberPad7",
N7: "NumberPad7",
NUMPAD8: "NumberPad8",
N8: "NumberPad8",
NUMPAD9: "NumberPad9",
N9: "NumberPad9",
MULTIPLY: "Multiply",
MUL: "Multiply",
ADD: "Add",
PLUS: "Add",
SEPARATOR: "Separator",
SEP: "Separator",
SUBTRACT: "Subtract",
MINUS: "Subtract",
DECIMAL: "Decimal",
PERIOD: "Decimal",
DIVIDE: "Divide",
DIV: "Divide",
F1: "F1",
F2: "F2",
F3: "F3",
F4: "F4",
F5: "F5",
F6: "F6",
F7: "F7",
F8: "F8",
F9: "F9",
F10: "F10",
F11: "F11",
F12: "F12",
META: "Meta",
COMMAND: "Command"
};
/**
* Returns a string representing the suite for this formatter language.
*
* @param testSuite the suite to format
* @param filename the file the formatted suite will be saved as
*/
function formatSuite(testSuite, filename) {
var suiteClass = /^(\w+)/.exec(filename)[1];
suiteClass = suiteClass[0].toUpperCase() + suiteClass.substring(1);
var formattedSuite = "using NXunit;\n"
+ "\n"
+ "namespace " + this.options.namespace + "\n"
+ '{\n'
+ indents(1) + "public class " + suiteClass + "\n"
+ indents(1) + '{\n'
+ indents(2) + "[Suite] public static TestSuite Suite\n"
+ indents(2) + '{\n'
+ indents(3) + "get\n"
+ indents(3) + '{\n'
+ indents(4) + 'TestSuite suite = new TestSuite("'+ suiteClass +'");\n';
for (var i = 0; i < testSuite.tests.length; ++i) {
var testClass = testSuite.tests[i].getTitle();
formattedSuite += indents(4)
+ "suite.Add(new " + testClass + "());\n";
}
formattedSuite += indents(4) + "return suite;\n"
+ indents(3) + "}\n"
+ indents(2) + "}\n"
+ indents(1) + "}\n"
+ "}\n";
return formattedSuite;
}
function defaultExtension() {
return this.options.defaultExtension;
}
this.options = {
receiver: "driver",
showSelenese: 'false',
namespace: "SeleniumTests",
indent: '4',
initialIndents: '3',
header:
'using System;\n' +
'using System.Text;\n' +
'using System.Text.RegularExpressions;\n' +
'using System.Threading;\n' +
'using OpenQA.Selenium;\n' +
'using OpenQA.Selenium.Firefox;\n' +
'using OpenQA.Selenium.Support.UI;\n' +
'using Xunit;\n' +
'\n' +
'namespace ${namespace}\n' +
'{\n' +
' public class ${className} : IDisposable\n' +
' {\n' +
' private IWebDriver driver;\n' +
' private StringBuilder verificationErrors;\n' +
' private string baseURL;\n' +
" private bool acceptNextAlert = true;\n" +
' \n' +
' public ${className}()\n' +
' {\n' +
' ${receiver} = new FirefoxDriver();\n' +
' baseURL = "${baseURL}";\n' +
' verificationErrors = new StringBuilder();\n' +
' }\n' +
' \n' +
' public void Dispose()\n' +
' {\n' +
' try\n' +
' {\n' +
' ${receiver}.Quit();\n' +
' }\n' +
' catch (Exception)\n' +
' {\n' +
' // Ignore errors if unable to close the browser\n' +
' }\n' +
' Assert.Equal("", verificationErrors.ToString());\n' +
' }\n' +
' \n' +
' [Fact]\n' +
' public void ${methodName}()\n' +
' {\n',
footer:
' }\n' +
" private bool IsElementPresent(By by)\n" +
" {\n" +
" try\n" +
" {\n" +
" driver.FindElement(by);\n" +
" return true;\n" +
" }\n" +
" catch (NoSuchElementException)\n" +
" {\n" +
" return false;\n" +
" }\n" +
" }\n" +
' \n' +
" private bool IsAlertPresent()\n" +
" {\n" +
" try\n" +
" {\n" +
" driver.SwitchTo().Alert();\n" +
" return true;\n" +
" }\n" +
" catch (NoAlertPresentException)\n" +
" {\n" +
" return false;\n" +
" }\n" +
" }\n" +
' \n' +
" private string CloseAlertAndGetItsText() {\n" +
" try {\n" +
" IAlert alert = driver.SwitchTo().Alert();\n" +
" string alertText = alert.Text;\n" +
" if (acceptNextAlert) {\n" +
" alert.Accept();\n" +
" } else {\n" +
" alert.Dismiss();\n" +
" }\n" +
" return alertText;\n" +
" } finally {\n" +
" acceptNextAlert = true;\n" +
" }\n" +
" }\n" +
' }\n' +
'}\n',
defaultExtension: "cs"
};
this.configForm = '<description>Variable for Selenium instance</description>' +
'<textbox id="options_receiver" />' +
'<description>Namespace</description>' +
'<textbox id="options_namespace" />' +
'<checkbox id="options_showSelenese" label="Show Selenese"/>';
this.name = "C# (WebDriver)";
this.testcaseExtension = ".cs";
this.suiteExtension = ".cs";
this.webdriver = true;
WDAPI.Driver = function() {
this.ref = options.receiver;
};
WDAPI.Driver.searchContext = function(locatorType, locator) {
var locatorString = xlateArgument(locator);
switch (locatorType) {
case 'xpath':
return 'By.XPath(' + locatorString + ')';
case 'css':
return 'By.CssSelector(' + locatorString + ')';
case 'id':
return 'By.Id(' + locatorString + ')';
case 'link':
return 'By.LinkText(' + locatorString + ')';
case 'name':
return 'By.Name(' + locatorString + ')';
case 'tag_name':
return 'By.TagName(' + locatorString + ')';
}
throw 'Error: unknown strategy [' + locatorType + '] for locator [' + locator + ']';
};
WDAPI.Driver.prototype.back = function() {
return this.ref + ".Navigate().Back()";
};
WDAPI.Driver.prototype.close = function() {
return this.ref + ".Close()";
};
WDAPI.Driver.prototype.findElement = function(locatorType, locator) {
return new WDAPI.Element(this.ref + ".FindElement(" + WDAPI.Driver.searchContext(locatorType, locator) + ")");
};
WDAPI.Driver.prototype.findElements = function(locatorType, locator) {
return new WDAPI.ElementList(this.ref + ".FindElements(" + WDAPI.Driver.searchContext(locatorType, locator) + ")");
};
WDAPI.Driver.prototype.getCurrentUrl = function() {
return this.ref + ".Url";
};
WDAPI.Driver.prototype.get = function(url) {
if (url.length > 1 && (url.substring(1,8) == "http://" || url.substring(1,9) == "https://")) { // url is quoted
return this.ref + ".Navigate().GoToUrl(" + url + ")";
} else {
return this.ref + ".Navigate().GoToUrl(baseURL + " + url + ")";
}
};
WDAPI.Driver.prototype.getTitle = function() {
return this.ref + ".Title";
};
WDAPI.Driver.prototype.getAlert = function() {
return "CloseAlertAndGetItsText()";
};
WDAPI.Driver.prototype.chooseOkOnNextConfirmation = function() {
return "acceptNextAlert = true";
};
WDAPI.Driver.prototype.chooseCancelOnNextConfirmation = function() {
return "acceptNextAlert = false";
};
WDAPI.Driver.prototype.refresh = function() {
return this.ref + ".Navigate().Refresh()";
};
WDAPI.Element = function(ref) {
this.ref = ref;
};
WDAPI.Element.prototype.clear = function() {
return this.ref + ".Clear()";
};
WDAPI.Element.prototype.click = function() {
return this.ref + ".Click()";
};
WDAPI.Element.prototype.getAttribute = function(attributeName) {
return this.ref + ".GetAttribute(" + xlateArgument(attributeName) + ")";
};
WDAPI.Element.prototype.getText = function() {
return this.ref + ".Text";
};
WDAPI.Element.prototype.isDisplayed = function() {
return this.ref + ".Displayed";
};
WDAPI.Element.prototype.isSelected = function() {
return this.ref + ".Selected";
};
WDAPI.Element.prototype.sendKeys = function(text) {
return this.ref + ".SendKeys(" + xlateArgument(text) + ")";
};
WDAPI.Element.prototype.submit = function() {
return this.ref + ".Submit()";
};
WDAPI.Element.prototype.select = function(selectLocator) {
if (selectLocator.type == 'index') {
return "new SelectElement(" + this.ref + ").SelectByIndex(" + selectLocator.string + ")";
}
if (selectLocator.type == 'value') {
return "new SelectElement(" + this.ref + ").SelectByValue(" + xlateArgument(selectLocator.string) + ")";
}
return "new Select(" + this.ref + ").SelectByText(" + xlateArgument(selectLocator.string) + ")";
};
WDAPI.ElementList = function(ref) {
this.ref = ref;
};
WDAPI.ElementList.prototype.getItem = function(index) {
return this.ref + "[" + index + "]";
};
WDAPI.ElementList.prototype.getSize = function() {
return this.ref + ".Count";
};
WDAPI.ElementList.prototype.isEmpty = function() {
return this.ref + ".Count == 0";
};
WDAPI.Utils = function() {
};
WDAPI.Utils.isElementPresent = function(how, what) {
return "IsElementPresent(" + WDAPI.Driver.searchContext(how, what) + ")";
};
WDAPI.Utils.isAlertPresent = function() {
return "IsAlertPresent()";
};
匯入 Selenium IDE
準備好 formatter 的執行語法後就需要將語法匯入 Selenium IDE 備用
Selenium IDE 主選單 Options –> Options
Formats –> Add
給個顯示名稱 –> 再把 script 貼進編輯視窗中 –> Save
存檔後,需要重開 Options –> Options ,剛加入的 formatter 才會出現
如何使用
使用細節可以參考 使用 Selenium IDE 與 C# 做 Web UI 測試 步驟大致如下:
- 使用 Selenium IDE 錄製 web 操作
- 匯出
.cs
並指定 foramtter - 將匯出的
.cs
加入測試專案 - 引用適合的 NuGet packages
參考資訊
文章作者 Yowko Tsai
上次更新 2021-10-26
授權合約
本部落格 (Yowko's Notes) 所有的文章內容(包含圖片),任何轉載行為,必須通知並獲本部落格作者 (Yowko Tsai) 的同意始得轉載,且轉載皆須註明出處與作者。
Yowko's Notes 由 Yowko Tsai 製作,以創用CC 姓名標示-非商業性-相同方式分享 3.0 台灣 授權條款 釋出。