Commit 7c2f9593 7c2f959312014493c2f544dbdf63a0fd106ddaf9 by Nicolas Perriault

refactored testing system, tests have been split in files and updated

1 parent c8a534cd
......@@ -660,11 +660,9 @@
* @return Casper
*/
start: function(location, then) {
if (this.started) {
this.log("start failed: Casper has already started!", "error");
}
this.log('Starting...', "info");
this.startTime = new Date().getTime();
this.history = [];
this.steps = [];
this.step = 0;
// Option checks
......
......@@ -45,7 +45,7 @@
'WARNING': { fg: 'red', bold: true },
'GREEN_BAR': { fg: 'white', bg: 'green', bold: true },
'RED_BAR': { fg: 'white', bg: 'red', bold: true },
'INFO_BAR': { fg: 'cyan', bold: true }
'INFO_BAR': { bg: 'cyan', fg: 'white', bold: true }
};
/**
......
......@@ -26,12 +26,17 @@
*
*/
(function(phantom) {
var fs = require('fs');
/**
* Casper tester: makes assertions, stores test results and display then.
*
*/
phantom.Casper.Tester = function(casper, options) {
this.running = false;
this.suites = [];
this.options = isType(options, "object") ? options : {};
if (!casper instanceof phantom.Casper) {
throw "phantom.Casper.Tester needs a phantom.Casper instance";
}
......@@ -41,21 +46,6 @@
var PASS = this.options.PASS || "PASS";
var FAIL = this.options.FAIL || "FAIL";
function compareArrays(a, b) {
if (a.length !== b.length) {
return false;
}
a.forEach(function(item, i) {
if (isType(item, "array") && !compareArrays(item, b[i])) {
return false;
}
if (item !== b[i]) {
return false;
}
});
return true;
}
// properties
this.testResults = {
passed: 0,
......@@ -229,6 +219,10 @@
return this.assertMatch(casper.getCurrentUrl(), pattern, message);
};
this.bar = function(text, style) {
casper.echo(fillBlanks(text), style);
};
/**
* Render a colorized output. Basically a proxy method for
* Casper.Colorizer#colorize()
......@@ -247,31 +241,11 @@
};
/**
* Tests equality between the two passed arguments.
* Declares the current test suite done.
*
* @param Mixed v1
* @param Mixed v2
* @param Boolean
*/
this.testEquals = function(v1, v2) {
if (betterTypeOf(v1) !== betterTypeOf(v2)) {
return false;
}
if (isType(v1, "function")) {
return v1.toString() === v2.toString();
}
if (v1 instanceof Object && v2 instanceof Object) {
if (Object.keys(v1).length !== Object.keys(v2).length) {
return false;
}
for (var k in v1) {
if (!this.testEquals(v1[k], v2[k])) {
return false;
}
}
return true;
}
return v1 === v2;
this.done = function() {
this.running = false;
};
/**
......@@ -284,6 +258,20 @@
};
/**
* Executes a file. We can't use phantom.injectJs(testFile) because we
* wouldn't be able to catch any thrown exception. So eval is evil, but
* evil actually works^W helps sometimes.
*
* @param String file Absolute path to some js/coffee file
*/
this.exec = function(file) {
if (!fs.isFile(file) || !this.isJsFile(file)) {
throw "Can only exec() files with .js or .coffee extensions";
}
eval(fs.read(file));
};
/**
* Adds a failed test entry to the stack.
*
* @param String message
......@@ -293,6 +281,31 @@
};
/**
* Recursively finds all test files contained in a given directory.
*
* @param String dir Path to some directory to scan
*/
this.findTestFiles = function(dir) {
var self = this;
if (!fs.isDirectory(dir)) {
return [];
}
var entries = fs.list(dir).filter(function(entry) {
return entry !== '.' && entry !== '..';
}).map(function(entry) {
return fs.absolute(pathJoin(dir, entry));
});
entries.forEach(function(entry) {
if (fs.isDirectory(entry)) {
entries = entries.concat(self.findTestFiles(entry));
}
});
return entries.filter(function(entry) {
return self.isJsFile(fs.absolute(pathJoin(dir, entry)));
});
};
/**
* Formats a message to highlight some parts of it.
*
* @param String message
......@@ -316,6 +329,20 @@
};
/**
* Checks if a file is apparently javascript compatible (.js or .coffee).
*
* @param String file Path to the file to test
* @return Boolean
*/
this.isJsFile = function(file) {
var ext;
try {
ext = file.split('.').pop().toLowerCase();
} catch(e) {}
return isType(ext, "string") && ['js', 'coffee'].indexOf(ext) !== -1;
};
/**
* Adds a successful test entry to the stack.
*
* @param String message
......@@ -340,13 +367,10 @@
style = 'GREEN_BAR';
}
result = statusText + ' ' + total + ' tests executed, ' + this.testResults.passed + ' passed, ' + this.testResults.failed + ' failed.';
if (result.length < 80) {
result += new Array(80 - result.length + 1).join(' ');
}
casper.echo(this.colorize(result, style));
casper.echo(this.colorize(fillBlanks(result), style));
if (save && isType(require, "function")) {
try {
require('fs').write(save, exporter.getXML(), 'w');
fs.write(save, exporter.getXML(), 'w');
casper.echo('result log stored in ' + save, 'INFO');
} catch (e) {
casper.echo('unable to write results to ' + save + '; ' + e, 'ERROR');
......@@ -356,5 +380,88 @@
casper.exit(status || 0);
}
};
/**
* Runs al suites contained in the paths passed as arguments.
*
*/
this.runSuites = function() {
var testFiles = [], self = this;
if (arguments.length === 0) {
throw "No test suite to run";
}
Array.prototype.forEach.call(arguments, function(path) {
if (!fs.exists(path)) {
self.bar("Path " + path + " doesn't exist", "RED_BAR");
}
if (fs.isDirectory(path)) {
testFiles = testFiles.concat(self.findTestFiles(path));
} else if (fs.isFile(path)) {
testFiles.push(path);
}
});
if (testFiles.length === 0) {
this.bar("No test file found, aborting.", "RED_BAR");
casper.exit(1);
}
var current = 0;
var interval = setInterval(function(self) {
if (self.running) {
return;
}
if (current === testFiles.length) {
self.renderResults(true);
clearInterval(interval);
} else {
self.runTest(testFiles[current]);
current++;
}
}, 100, this);
};
/**
* Runs a test file
*
*/
this.runTest = function(testFile) {
this.bar('Test file: ' + testFile, 'INFO_BAR');
this.running = true;
try {
this.exec(testFile);
} catch (e) {
// TODO: better formatting of aborted failing suite
// TODO: add exception trace (?)
this.error('FATAL: exception raised while runing test suite in ' + testFile + ': ' + e);
this.done();
}
};
/**
* Tests equality between the two passed arguments.
*
* @param Mixed v1
* @param Mixed v2
* @param Boolean
*/
this.testEquals = function(v1, v2) {
if (betterTypeOf(v1) !== betterTypeOf(v2)) {
return false;
}
if (isType(v1, "function")) {
return v1.toString() === v2.toString();
}
if (v1 instanceof Object && v2 instanceof Object) {
if (Object.keys(v1).length !== Object.keys(v2).length) {
return false;
}
for (var k in v1) {
if (!this.testEquals(v1[k], v2[k])) {
return false;
}
}
return true;
}
return v1 === v2;
};
};
})(phantom);
\ No newline at end of file
})(phantom);
......
......@@ -136,6 +136,36 @@ function createPage(casper) {
}
/**
* Dumps a JSON representation of passed value to the console. Used for
* debugging purpose only.
*
* @param Mixed value
*/
function dump(value) {
if (isType(value, "array")) {
value = value.map(function(prop) {
return isType(prop, "function") ? prop.toString().replace(/\s{2,}/, '') : prop;
});
}
console.log(JSON.stringify(value, null, 4));
}
/**
* Takes a string and append blank until the pad value is reached.
*
* @param String text
* @param Number pad Pad value (optional; default: 80)
* @return String
*/
function fillBlanks(text, pad) {
pad = pad || 80;
if (text.length < pad) {
text += new Array(pad - text.length + 1).join(' ');
}
return text;
}
/**
* Shorthands for checking if a value is of the given type. Can check for
* arrays.
*
......
(function(t) {
var fs = require('fs'), testFile = '/tmp/__casper_test_capture.png';
if (fs.exists(testFile) && fs.isFile(testFile)) {
fs.remove(testFile);
}
casper.start('tests/site/index.html', function(self) {
self.viewport(300, 200);
t.comment('Casper.capture()');
self.capture(testFile);
t.assert(fs.isFile(testFile), 'Casper.capture() captured a screenshot');
});
try {
fs.remove(testFile);
} catch(e) {}
casper.run(function(self) {
t.done();
});
})(casper.test);
(function(t) {
t.comment('Casper.click()');
casper.start('tests/site/index.html', function(self) {
self.click('a[href="test.html"]');
});
casper.then(function(self) {
t.assertTitle('CasperJS test target', 'Casper.click() can click on a link');
}).thenClick('a', function(self) {
t.assertTitle('CasperJS test form', 'Casper.thenClick() can click on a link');
});
casper.run(function(self) {
t.done();
});
})(casper.test);
(function(t) {
t.comment('Casper.base64encode()');
casper.start('tests/site/index.html', function(self) {
var image = self.base64encode('file://' + phantom.libraryPath + '/site/images/phantom.png');
t.assertEquals(image.length, 6160, 'Casper.base64encode() can retrieve base64 contents');
});
casper.run(function(self) {
t.done();
});
})(casper.test);
\ No newline at end of file
(function(t) {
t.comment('Casper.evaluate()');
casper.start();
var params = {
"boolean true": true,
"boolean false": false,
"int number": 42,
"float number": 1337.42,
"string": "plop! \"Ÿ£$\" 'no'",
"array": [1, 2, 3],
"object": {a: 1, b: 2}
};
var casperParams = casper.evaluate(function() {
return __casper_params__;
}, params);
casper.test.assertType(casperParams, "object", 'Casper.evaluate() exposes parameters in a dedicated object');
casper.test.assertEquals(Object.keys(casperParams).length, 7, 'Casper.evaluate() object containing parameters has the correct length');
for (var param in casperParams) {
casper.test.assertEquals(JSON.stringify(casperParams[param]), JSON.stringify(params[param]), 'Casper.evaluate() can pass a ' + param);
casper.test.assertEquals(typeof casperParams[param], typeof params[param], 'Casper.evaluate() preserves the ' + param + ' type');
}
t.done();
})(casper.test);
(function(t) {
t.comment('Casper.exists()');
casper.start('tests/site/index.html', function(self) {
t.assert(self.exists('a') && !self.exists('chucknorriz'), 'Casper.exists() can check if an element exists');
});
casper.run(function(step) {
t.done();
});
})(casper.test);
(function(t) {
t.comment('Casper.fetchText()');
casper.start('tests/site/index.html', function(self) {
t.assertEquals(self.fetchText('ul li'), 'onetwothree', 'Casper.fetchText() can retrieves text contents');
});
casper.run(function(self) {
t.done();
});
})(casper.test);
(function(t) {
casper.start('tests/site/form.html', function(self) {
t.comment('Casper.fill()');
self.fill('form[action="result.html"]', {
email: 'chuck@norris.com',
content: 'Am watching thou',
check: true,
choice: 'no',
topic: 'bar',
file: phantom.libraryPath + '/README.md',
'checklist[]': ['1', '3']
});
t.assertEvalEquals(function() {
return document.querySelector('input[name="email"]').value;
}, 'chuck@norris.com', 'Casper.fill() can fill an input[type=text] form field');
t.assertEvalEquals(function() {
return document.querySelector('textarea[name="content"]').value;
}, 'Am watching thou', 'Casper.fill() can fill a textarea form field');
t.assertEvalEquals(function() {
return document.querySelector('select[name="topic"]').value;
}, 'bar', 'Casper.fill() can pick a value from a select form field');
t.assertEvalEquals(function() {
return document.querySelector('input[name="check"]').checked;
}, true, 'Casper.fill() can check a form checkbox');
t.assertEvalEquals(function() {
return document.querySelector('input[name="choice"][value="no"]').checked;
}, true, 'Casper.fill() can check a form radio button 1/2');
t.assertEvalEquals(function() {
return document.querySelector('input[name="choice"][value="yes"]').checked;
}, false, 'Casper.fill() can check a form radio button 2/2');
t.assertEvalEquals(function() {
return document.querySelector('input[name="file"]').files.length === 1;
}, true, 'Casper.fill() can select a file to upload');
t.assertEvalEquals(function() {
return (document.querySelector('input[name="checklist[]"][value="1"]').checked &&
!document.querySelector('input[name="checklist[]"][value="2"]').checked &&
document.querySelector('input[name="checklist[]"][value="3"]').checked);
}, true, 'Casper.fill() can fill a list of checkboxes');
self.click('input[type="submit"]');
});
casper.then(function(self) {
t.comment('Form submitted');
t.assertUrlMatch(/email=chuck@norris.com/, 'Casper.fill() input[type=email] field was submitted');
t.assertUrlMatch(/content=Am\+watching\+thou/, 'Casper.fill() textarea field was submitted');
t.assertUrlMatch(/check=on/, 'Casper.fill() input[type=checkbox] field was submitted');
t.assertUrlMatch(/choice=no/, 'Casper.fill() input[type=radio] field was submitted');
t.assertUrlMatch(/topic=bar/, 'Casper.fill() select field was submitted');
});
casper.run(function(self) {
t.done();
});
})(casper.test);
(function(t) {
casper.start('tests/site/global.html', function(self) {
t.comment('Casper.getGlobal()');
t.assertEquals(self.getGlobal('myGlobal'), 'awesome string', 'Casper.getGlobal() can retrieve a remote global variable');
t.assertRaises(self.getGlobal, ['myUnencodableGlobal'], 'Casper.getGlobal() does not fail trying to encode an unencodable global');
});
casper.run(function(self) {
t.done();
});
})(casper.test);
(function(t) {
casper.start('tests/site/page1.html');
casper.thenOpen('tests/site/page2.html');
casper.thenOpen('tests/site/page3.html');
casper.back();
casper.then(function(self) {
t.comment('navigating history backward');
t.assertMatch(self.getCurrentUrl(), /tests\/site\/page2\.html$/, 'Casper.back() can go back an history step');
});
casper.forward();
casper.then(function(self) {
t.comment('navigating history forward');
t.assertMatch(self.getCurrentUrl(), /tests\/site\/page3\.html$/, 'Casper.forward() can go forward an history step');
});
casper.run(function(self) {
t.assert(self.history.length > 0, 'Casper.history contains urls');
t.assertMatch(self.history[0], /tests\/site\/page1\.html$/, 'Casper.history has the correct first url');
t.done();
});
})(casper.test);
(function(t) {
// Casper.options.onStepComplete
casper.start('tests/site/index.html', function(self) {
self.options.onStepComplete = function(self, stepResult) {
t.comment('Casper.options.onStepComplete()');
t.assertEquals(stepResult, 'ok', 'Casper.options.onStepComplete() is called on step complete');
self.options.onStepComplete = null;
};
return 'ok';
});
// Casper.options.onResourceRequested & Casper.options.onResourceReceived
casper.then(function(self) {
self.options.onResourceReceived = function(self, resource) {
t.comment('Casper.options.onResourceReceived()');
t.assertType(resource, 'object', 'Casper.options.onResourceReceived() retrieve a resource object');
t.assert('status' in resource, 'Casper.options.onResourceReceived() retrieve a valid resource object');
self.options.onResourceReceived = null;
};
self.options.onResourceRequested = function(self, request) {
t.comment('Casper.options.onResourceRequested()');
t.assertType(request, 'object', 'Casper.options.onResourceRequested() retrieve a request object');
t.assert('method' in request, 'Casper.options.onResourceRequested() retrieve a valid request object');
self.options.onResourceRequested = null;
};
self.thenOpen('tests/site/page1.html');
});
// Casper.options.onAlert()
casper.then(function(self) {
self.options.onAlert = function(self, message) {
t.assertEquals(message, 'plop', 'Casper.options.onAlert() can intercept an alert message');
};
}).thenOpen('tests/site/alert.html').click('button', function(self) {
self.options.onAlert = null;
});
casper.run(function(self) {
t.done();
});
})(casper.test);
(function(t) {
casper.start('tests/site/index.html');
var oldLevel = casper.options.logLevel;
casper.options.logLevel = 'info';
casper.options.verbose = false;
t.comment('Casper.log()');
casper.log('foo', 'info');
t.assert(casper.result.log.some(function(e) {
return e.message === 'foo' && e.level === 'info';
}), 'Casper.log() adds a log entry');
casper.options.logLevel = oldLevel;
casper.options.verbose = true;
casper.then(function(self) {
var oldLevel = casper.options.logLevel;
casper.options.logLevel = 'debug';
casper.options.verbose = false;
casper.evaluate(function() {
__utils__.log('debug message');
__utils__.log('info message', 'info');
});
t.assert(casper.result.log.some(function(e) {
return e.message === 'debug message' && e.level === 'debug' && e.space === 'remote';
}), 'ClientUtils.log() adds a log entry');
t.assert(casper.result.log.some(function(e) {
return e.message === 'info message' && e.level === 'info' && e.space === 'remote';
}), 'ClientUtils.log() adds a log entry at a given level');
casper.options.logLevel = oldLevel;
casper.options.verbose = true;
});
casper.run(function(self) {
t.assertEquals(self.result.log.length, 3, 'Casper.log() logged messages');
t.done();
});
})(casper.test);
(function(t) {
t.comment('Casper.start()');
casper.start('tests/site/index.html', function(self) {
t.pass('Casper.start() can chain a next step');
t.assertTitle('CasperJS test index', 'Casper.start() opened the passed url');
t.assertEval(function() {
return typeof(__utils__) === "object";
}, 'Casper.start() injects ClientUtils instance within remote DOM');
});
t.assert(casper.started, 'Casper.start() started');
casper.run(function(self) {
t.done();
});
})(casper.test);
(function(t) {
t.comment('Casper.then()');
casper.start('tests/site/index.html');
var nsteps = casper.steps.length;
casper.then(function(self) {
t.assertTitle('CasperJS test index', 'Casper.then() added a new step');
});
t.assertEquals(casper.steps.length, nsteps + 1, 'Casper.then() can add a new step');
t.comment('Casper.thenOpen()');
casper.thenOpen('tests/site/test.html');
t.assertEquals(casper.steps.length, nsteps + 2, 'Casper.thenOpen() can add a new step');
casper.thenOpen('tests/site/test.html', function(self) {
t.assertTitle('CasperJS test target', 'Casper.thenOpen() opened a location and executed a step');
});
t.assertEquals(casper.steps.length, nsteps + 4, 'Casper.thenOpen() can add a new step for opening, plus another step');
t.comment('Casper.each()');
casper.each([1, 2, 3], function(self, item, i) {
self.test.assertEquals(i, item - 1, 'Casper.each() passes a contextualized index');
});
casper.run(function(self) {
t.done();
});
})(casper.test);
(function(t) {
t.comment('Casper.viewport()');
casper.start();
casper.viewport(1337, 999);
t.assertEquals(casper.page.viewportSize.width, 1337, 'Casper.viewport() can change the width of page viewport');
t.assertEquals(casper.page.viewportSize.height, 999, 'Casper.viewport() can change the height of page viewport');
t.assertRaises(casper.viewport, ['a', 'b'], 'Casper.viewport() validates viewport size data');
t.done();
})(casper.test);
(function(t) {
casper.start('tests/site/visible.html', function(self) {
self.test.comment('Casper.visible()');
self.test.assert(self.visible('#img1'), 'Casper.visible() can detect if an element is visible');
self.test.assert(!self.visible('#img2'), 'Casper.visible() can detect if an element is invisible');
self.test.assert(!self.visible('#img3'), 'Casper.visible() can detect if an element is invisible');
self.waitWhileVisible('#img1', function(self) {
self.test.comment('Casper.waitWhileVisible()');
self.test.pass('Casper.waitWhileVisible() can wait while an element is visible');
}, function(self) {
self.test.comment('Casper.waitWhileVisible()');
self.test.fail('Casper.waitWhileVisible() can wait while an element is visible');
}, 2000);
});
casper.run(function(self) {
t.done();
});
})(casper.test);
(function(t) {
var waitStart;
casper.start('tests/site/index.html', function(self) {
waitStart = new Date().getTime();
});
casper.wait(1000, function(self) {
self.test.comment('Casper.wait()');
self.test.assert(new Date().getTime() - waitStart > 1000, 'Casper.wait() can wait for a given amount of time');
// Casper.waitFor()
casper.thenOpen('tests/site/waitFor.html', function(self) {
casper.test.comment('Casper.waitFor()');
self.waitFor(function(self) {
return self.evaluate(function() {
return document.querySelectorAll('li').length === 4;
});
}, function(self) {
self.test.pass('Casper.waitFor() can wait for something to happen');
}, function(self) {
self.test.fail('Casper.waitFor() can wait for something to happen');
});
});
});
casper.run(function(self) {
t.done();
});
})(casper.test);
// phantom.Casper.FunctionArgsInjector
var t = casper.test;
function createInjector(fn, values) {
return new phantom.Casper.FunctionArgsInjector(fn, values);
}
var testFn = function(a, b) { return a + b; };
var injector = createInjector(testFn);
var extract = injector.extract(testFn);
t.comment('FunctionArgsInjector.extract()');
t.assertType(extract, "object", 'FunctionArgsInjector.extract() returns an object');
t.assertEquals(extract.name, null, 'FunctionArgsInjector.extract() process function name as expected');
t.assertEquals(extract.body, 'return a + b;', 'FunctionArgsInjector.extract() process function body as expected');
t.assertEquals(extract.args, ['a', 'b'], 'FunctionArgsInjector.extract() process function args as expected');
var processed;
eval('processed = ' + injector.process({ a: 1, b: 2 }));
t.assertEquals(processed(), 3, 'FunctionArgsInjector.process() proccessed the function correctly');
t.done();
\ No newline at end of file
var t = casper.test;
t.comment('Tester.testEquals()');
t.assert(casper.test.testEquals(null, null), 'Tester.testEquals() null equality');
t.assertNot(casper.test.testEquals(null, undefined), 'Tester.testEquals() null vs. undefined inequality');
t.assert(casper.test.testEquals("hi", "hi"), 'Tester.testEquals() string equality');
t.assertNot(casper.test.testEquals("hi", "ih"), 'Tester.testEquals() string inequality');
t.assert(casper.test.testEquals(5, 5), 'Tester.testEquals() number equality');
t.assert(casper.test.testEquals(5, 5.0), 'Tester.testEquals() cast number equality');
t.assertNot(casper.test.testEquals(5, 10), 'Tester.testEquals() number inequality');
t.assert(casper.test.testEquals([], []), 'Tester.testEquals() empty array equality');
t.assert(casper.test.testEquals([1,2], [1,2]), 'Tester.testEquals() array equality');
t.assert(casper.test.testEquals([1,2,[1,2,function(){}]], [1,2,[1,2,function(){}]]), 'Tester.testEquals() complex array equality');
t.assertNot(casper.test.testEquals([1,2,[1,2,function(a){}]], [1,2,[1,2,function(b){}]]), 'Tester.testEquals() complex array inequality');
t.assertNot(casper.test.testEquals([1,2], [2,1]), 'Tester.testEquals() shuffled array inequality');
t.assertNot(casper.test.testEquals([1,2], [1,2,3]), 'Tester.testEquals() array length inequality');
t.assert(casper.test.testEquals({}, {}), 'Tester.testEquals() empty object equality');
t.assert(casper.test.testEquals({a:1,b:2}, {a:1,b:2}), 'Tester.testEquals() object length equality');
t.assert(casper.test.testEquals({a:1,b:2}, {b:2,a:1}), 'Tester.testEquals() shuffled object keys equality');
t.assertNot(casper.test.testEquals({a:1,b:2}, {a:1,b:3}), 'Tester.testEquals() object inequality');
t.assert(casper.test.testEquals({1:{name:"bob",age:28}, 2:{name:"john",age:26}}, {1:{name:"bob",age:28}, 2:{name:"john",age:26}}), 'Tester.testEquals() complex object equality');
t.assertNot(casper.test.testEquals({1:{name:"bob",age:28}, 2:{name:"john",age:26}}, {1:{name:"bob",age:28}, 2:{name:"john",age:27}}), 'Tester.testEquals() complex object inequality');
t.assert(casper.test.testEquals(function(x){return x;}, function(x){return x;}), 'Tester.testEquals() function equality');
t.assertNot(casper.test.testEquals(function(x){return x;}, function(y){return y+2;}), 'Tester.testEquals() function inequality');
t.done();
\ No newline at end of file
var t = casper.test;
t.comment('phantom.Casper.XUnitExporter');
xunit = new phantom.Casper.XUnitExporter();
xunit.addSuccess('foo', 'bar');
t.assertMatch(xunit.getXML(), /<testcase classname="foo" name="bar"/, 'XUnitExporter.addSuccess() adds a successful testcase');
xunit.addFailure('bar', 'baz', 'wrong', 'chucknorriz');
t.assertMatch(xunit.getXML(), /<testcase classname="bar" name="baz"><failure type="chucknorriz">wrong/, 'XUnitExporter.addFailure() adds a failed testcase');
t.done();
\ No newline at end of file