diff --git a/README.md b/README.md
index f5f2198..454f61d 100755
--- a/README.md
+++ b/README.md
@@ -62,6 +62,11 @@ Please refer to the [e2e_spec.coffee](spec/e2e_spec.coffee) for more details on
## Changelog
+### 3.2.0
+
+- Support name and test count attributes for the root test suites element. Thanks to [Simeon Cheeseman](https://github.com/SimeonC).
+- Describe parameter types and return types with JSDoc. Thanks to [Simeon Cheeseman](https://github.com/SimeonC).
+
### 3.1.0
- Add support for generic properties for test cases. Thanks to [Pietro Ferrulli](https://github.com/Pi-fe).
diff --git a/spec/e2e_spec.js b/spec/e2e_spec.js
index cda7d2e..b184f90 100644
--- a/spec/e2e_spec.js
+++ b/spec/e2e_spec.js
@@ -14,8 +14,21 @@ describe('JUnit Report builder', function () {
}),
);
- const reportWith = (content) =>
- '\n' + '\n' + content + '\n' + '';
+ const parseProperties = (properties = {}) => {
+ let result = '';
+ ['tests', 'failures', 'errors', 'skipped'].forEach((key) => {
+ result += key + '="' + (properties[key] || 0) + '" ';
+ });
+ for (const key in properties) {
+ if (['tests', 'failures', 'errors', 'skipped'].includes(key)) {
+ continue;
+ }
+ result += key + '="' + properties[key] + '" ';
+ }
+ return result.trim();
+ };
+
+ const reportWith = (content, testSuitesProperties) => '\n' + content;
it('should produce a report identical to the expected one', function () {
builder.testCase().className('root.test.Class1');
@@ -40,12 +53,22 @@ describe('JUnit Report builder', function () {
expect(actual).toBe(expected);
});
- it('should produce an empty list of test suites when nothing reported', () =>
+ it('should produce an empty list of test suites when nothing reported', () => {
+ expect(builder.build()).toBe(
+ // prettier-ignore
+ '\n' +
+ '',
+ );
+ });
+
+ it('should set testsuites name', () => {
+ builder.name('testSuitesName');
expect(builder.build()).toBe(
// prettier-ignore
'\n' +
- '',
- ));
+ '',
+ );
+ });
it('should produce an empty test suite when a test suite reported', function () {
builder.testSuite();
@@ -53,7 +76,9 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
- ' ',
+ '\n' +
+ ' \n' +
+ '',
),
);
});
@@ -64,7 +89,9 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
- ' ',
+ '\n' +
+ ' \n' +
+ '',
),
);
});
@@ -75,9 +102,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -88,9 +117,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -101,9 +132,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -114,9 +147,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -127,9 +162,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -140,11 +177,13 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -155,11 +194,13 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -170,11 +211,13 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -185,7 +228,9 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
- ' ',
+ '\n' +
+ ' \n' +
+ '',
),
);
});
@@ -196,7 +241,9 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
- ' ',
+ '\n' +
+ ' \n' +
+ '',
),
);
});
@@ -207,9 +254,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -220,11 +269,13 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -235,11 +286,13 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -254,6 +307,7 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
' \n' +
@@ -261,7 +315,8 @@ describe('JUnit Report builder', function () {
' [[ATTACHMENT|absolute/path/to/attachment]]\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -274,9 +329,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -287,9 +344,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -300,9 +359,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
@@ -313,9 +374,11 @@ describe('JUnit Report builder', function () {
expect(builder.build()).toBe(
reportWith(
// prettier-ignore
+ '\n' +
' \n' +
' \n' +
- ' ',
+ ' \n' +
+ '',
),
);
});
diff --git a/spec/expected_report.xml b/spec/expected_report.xml
index 5ef522d..d0ffe04 100755
--- a/spec/expected_report.xml
+++ b/spec/expected_report.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/spec/test_case_spec.js b/spec/test_case_spec.js
index a3ad455..d81e15d 100644
--- a/spec/test_case_spec.js
+++ b/spec/test_case_spec.js
@@ -1,4 +1,4 @@
-const TestCase = require('../src/test_case');
+const { TestCase } = require('../src/test_case');
describe('Test Case builder', function () {
let testCase = null;
diff --git a/spec/test_suite_spec.js b/spec/test_suite_spec.js
index dc5fe69..bca4d4e 100644
--- a/spec/test_suite_spec.js
+++ b/spec/test_suite_spec.js
@@ -1,4 +1,4 @@
-const TestSuite = require('../src/test_suite');
+const { TestSuite } = require('../src/test_suite');
describe('Test Suite builder', function () {
let testSuite = null;
@@ -9,7 +9,13 @@ describe('Test Suite builder', function () {
beforeEach(function () {
const factory = jasmine.createSpyObj('factory', ['newTestCase']);
- testCase = jasmine.createSpyObj('testCase', ['build', 'getFailureCount', 'getErrorCount', 'getSkippedCount']);
+ testCase = jasmine.createSpyObj('testCase', [
+ 'build',
+ 'getFailureCount',
+ 'getErrorCount',
+ 'getSkippedCount',
+ 'getTestCaseCount',
+ ]);
factory.newTestCase.and.callFake(() => testCase);
@@ -33,6 +39,8 @@ describe('Test Suite builder', function () {
}
});
+ testCase.getTestCaseCount.and.callFake(() => 1);
+
testCase.getFailureCount.and.callFake(() => 0);
testCase.getErrorCount.and.callFake(() => 0);
diff --git a/src/builder.js b/src/builder.js
index 017b5cc..f08b8fe 100644
--- a/src/builder.js
+++ b/src/builder.js
@@ -1,43 +1,64 @@
-var _ = require('lodash');
-var xmlBuilder = require('xmlbuilder');
+// @ts-check
var path = require('path');
var makeDir = require('make-dir');
var fs = require('fs');
-var TestSuite = require('./test_suite');
-var TestCase = require('./test_case');
+var { TestSuites } = require('./test_suites');
-function JUnitReportBuilder(factory) {
- this._factory = factory;
- this._testSuitesAndCases = [];
+class JUnitReportBuilder {
+ /**
+ * @param {import('./factory').Factory} factory
+ */
+ constructor(factory) {
+ this._factory = factory;
+ this._rootTestSuites = new TestSuites(factory);
+ }
+
+ /**
+ * @param {string} reportPath
+ */
+ writeTo(reportPath) {
+ makeDir.sync(path.dirname(reportPath));
+ fs.writeFileSync(reportPath, this.build(), 'utf8');
+ }
+
+ /**
+ * @returns {string}
+ */
+ build() {
+ var xmlTree = this._rootTestSuites.build();
+ return xmlTree.end({ pretty: true });
+ }
+
+ /**
+ * @param {string} name
+ * @returns {JUnitReportBuilder}
+ * @chainable
+ */
+ name(name) {
+ this._rootTestSuites.name(name);
+ return this;
+ }
+
+ /**
+ * @returns {import('./test_suite').TestSuite}
+ */
+ testSuite() {
+ return this._rootTestSuites.testSuite();
+ }
+
+ /**
+ * @returns {import('./test_case').TestCase}
+ */
+ testCase() {
+ return this._rootTestSuites.testCase();
+ }
+
+ /**
+ * @returns {JUnitReportBuilder}
+ */
+ newBuilder() {
+ return this._factory.newBuilder();
+ }
}
-JUnitReportBuilder.prototype.writeTo = function (reportPath) {
- makeDir.sync(path.dirname(reportPath));
- fs.writeFileSync(reportPath, this.build(), 'utf8');
-};
-
-JUnitReportBuilder.prototype.build = function () {
- var xmlTree = xmlBuilder.create('testsuites', { encoding: 'UTF-8', invalidCharReplacement: '' });
- _.forEach(this._testSuitesAndCases, function (suiteOrCase) {
- suiteOrCase.build(xmlTree);
- });
- return xmlTree.end({ pretty: true });
-};
-
-JUnitReportBuilder.prototype.testSuite = function () {
- var suite = this._factory.newTestSuite();
- this._testSuitesAndCases.push(suite);
- return suite;
-};
-
-JUnitReportBuilder.prototype.testCase = function () {
- var testCase = this._factory.newTestCase();
- this._testSuitesAndCases.push(testCase);
- return testCase;
-};
-
-JUnitReportBuilder.prototype.newBuilder = function () {
- return this._factory.newBuilder();
-};
-
-module.exports = JUnitReportBuilder;
+module.exports = { Builder: JUnitReportBuilder };
diff --git a/src/factory.js b/src/factory.js
index 4bbeffb..d17d36e 100644
--- a/src/factory.js
+++ b/src/factory.js
@@ -1,19 +1,36 @@
-var Builder = require('./builder');
-var TestSuite = require('./test_suite');
-var TestCase = require('./test_case');
+var { Builder } = require('./builder');
+var { TestSuites } = require('./test_suites');
+var { TestSuite } = require('./test_suite');
+var { TestCase } = require('./test_case');
-function Factory() {}
+class Factory {
+ /**
+ * @returns {import('./builder').Builder}
+ */
+ newBuilder() {
+ return new Builder(this);
+ }
-Factory.prototype.newBuilder = function () {
- return new Builder(this);
-};
+ /**
+ * @returns {import('./test_suite').TestSuite}
+ */
+ newTestSuite() {
+ return new TestSuite(this);
+ }
-Factory.prototype.newTestSuite = function () {
- return new TestSuite(this);
-};
+ /**
+ * @returns {import('./test_case').TestCase}
+ */
+ newTestCase() {
+ return new TestCase(this);
+ }
-Factory.prototype.newTestCase = function () {
- return new TestCase(this);
-};
+ /**
+ * @returns {import('./test_suites').TestSuites}
+ */
+ newTestSuites() {
+ return new TestSuites(this);
+ }
+}
-module.exports = Factory;
+module.exports = { Factory: Factory };
diff --git a/src/index.js b/src/index.js
index af0080b..efb1484 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,3 @@
-var Factory = require('./factory');
+var { Factory } = require('./factory');
module.exports = new Factory().newBuilder();
diff --git a/src/test_case.js b/src/test_case.js
index bb1b900..666f3be 100755
--- a/src/test_case.js
+++ b/src/test_case.js
@@ -1,144 +1,195 @@
+// @ts-check
var _ = require('lodash');
-function TestCase() {
- this._error = false;
- this._failure = false;
- this._skipped = false;
- this._standardOutput = undefined;
- this._standardError = undefined;
- this._stacktrace = undefined;
- this._attributes = {};
- this._errorAttributes = {};
- this._failureAttributes = {};
- this._errorAttachment = undefined;
- this._errorContent = undefined;
- this._properties = [];
-}
+var { TestNode } = require('./test_node');
+
+class TestCase extends TestNode {
+ /**
+ * @param {import('./factory').Factory} factory
+ */
+ constructor(factory) {
+ super(factory, 'testcase');
+ this._error = false;
+ this._failure = false;
+ this._skipped = false;
+ this._standardOutput = undefined;
+ this._standardError = undefined;
+ this._stacktrace = undefined;
+ this._errorAttributes = {};
+ this._failureAttributes = {};
+ this._errorAttachment = undefined;
+ this._errorContent = undefined;
+ }
-TestCase.prototype.className = function (className) {
- this._attributes.classname = className;
- return this;
-};
-
-TestCase.prototype.name = function (name) {
- this._attributes.name = name;
- return this;
-};
-
-TestCase.prototype.time = function (timeInSeconds) {
- this._attributes.time = timeInSeconds;
- return this;
-};
-
-TestCase.prototype.file = function (filepath) {
- this._attributes.file = filepath;
- return this;
-};
-
-TestCase.prototype.failure = function (message, type) {
- this._failure = true;
- if (message) {
- this._failureAttributes.message = message;
+ /**
+ * @param {string} className
+ * @returns {TestCase}
+ * @chainable
+ */
+ className(className) {
+ this._attributes.classname = className;
+ return this;
}
- if (type) {
- this._failureAttributes.type = type;
+
+ /**
+ * @param {string} filepath
+ * @returns {TestCase}
+ * @chainable
+ */
+ file(filepath) {
+ this._attributes.file = filepath;
+ return this;
}
- return this;
-};
-TestCase.prototype.error = function (message, type, content) {
- this._error = true;
- if (message) {
- this._errorAttributes.message = message;
+ /**
+ * @param {string} [message]
+ * @param {string} [type]
+ * @returns {TestCase}
+ * @chainable
+ */
+ failure(message, type) {
+ this._failure = true;
+ if (message) {
+ this._failureAttributes.message = message;
+ }
+ if (type) {
+ this._failureAttributes.type = type;
+ }
+ return this;
}
- if (type) {
- this._errorAttributes.type = type;
+
+ /**
+ * @param {string} [message]
+ * @param {string} [type]
+ * @param {string} [content]
+ * @returns {TestCase}
+ * @chainable
+ */
+ error(message, type, content) {
+ this._error = true;
+ if (message) {
+ this._errorAttributes.message = message;
+ }
+ if (type) {
+ this._errorAttributes.type = type;
+ }
+ if (content) {
+ this._errorContent = content;
+ }
+ return this;
}
- if (content) {
- this._errorContent = content;
+
+ /**
+ * @param {string} [stacktrace]
+ * @returns {TestCase}
+ * @chainable
+ */
+ stacktrace(stacktrace) {
+ this._failure = true;
+ this._stacktrace = stacktrace;
+ return this;
}
- return this;
-};
-
-TestCase.prototype.stacktrace = function (stacktrace) {
- this._failure = true;
- this._stacktrace = stacktrace;
- return this;
-};
-
-TestCase.prototype.skipped = function () {
- this._skipped = true;
- return this;
-};
-
-TestCase.prototype.standardOutput = function (log) {
- this._standardOutput = log;
- return this;
-};
-
-TestCase.prototype.standardError = function (log) {
- this._standardError = log;
- return this;
-};
-
-TestCase.prototype.getFailureCount = function () {
- return Number(this._failure);
-};
-
-TestCase.prototype.getErrorCount = function () {
- return Number(this._error);
-};
-
-TestCase.prototype.getSkippedCount = function () {
- return Number(this._skipped);
-};
-
-TestCase.prototype.errorAttachment = function (path) {
- this._errorAttachment = path;
- return this;
-};
-
-TestCase.prototype.property = function (name, value) {
- this._properties.push({ name: name, value: value });
- return this;
-};
-
-TestCase.prototype.build = function (parentElement) {
- var testCaseElement = parentElement.ele('testcase', this._attributes);
- if (this._properties.length) {
- var propertiesElement = testCaseElement.ele('properties');
- _.forEach(this._properties, function (property) {
- propertiesElement.ele('property', {
- name: property.name,
- value: property.value,
- });
- });
+
+ /**
+ * @returns {TestCase}
+ * @chainable
+ */
+ skipped() {
+ this._skipped = true;
+ return this;
}
- if (this._failure) {
- var failureElement = testCaseElement.ele('failure', this._failureAttributes);
- if (this._stacktrace) {
- failureElement.cdata(this._stacktrace);
- }
+
+ /**
+ * @param {string} [log]
+ * @returns {TestCase}
+ * @chainable
+ */
+ standardOutput(log) {
+ this._standardOutput = log;
+ return this;
}
- if (this._error) {
- var errorElement = testCaseElement.ele('error', this._errorAttributes);
- if (this._errorContent) {
- errorElement.cdata(this._errorContent);
- }
+
+ /**
+ * @param {string} [log]
+ * @returns {TestCase}
+ * @chainable
+ */
+ standardError(log) {
+ this._standardError = log;
+ return this;
}
- if (this._skipped) {
- testCaseElement.ele('skipped');
+
+ /**
+ * @returns {number}
+ */
+ getTestCaseCount() {
+ return 1;
+ }
+
+ /**
+ * @returns {number}
+ */
+ getFailureCount() {
+ return Number(this._failure);
+ }
+
+ /**
+ * @returns {number}
+ */
+ getErrorCount() {
+ return Number(this._error);
+ }
+
+ /**
+ * @returns {number}
+ */
+ getSkippedCount() {
+ return Number(this._skipped);
}
- if (this._standardOutput) {
- testCaseElement.ele('system-out').cdata(this._standardOutput);
+
+ /**
+ *
+ * @param {string} path
+ * @returns {TestCase}
+ * @chainable
+ */
+ errorAttachment(path) {
+ this._errorAttachment = path;
+ return this;
}
- var systemError;
- if (this._standardError) {
- systemError = testCaseElement.ele('system-err').cdata(this._standardError);
- if (this._errorAttachment) {
- systemError.txt('[[ATTACHMENT|' + this._errorAttachment + ']]');
+ /**
+ * @param {import('xmlbuilder').XMLElement} parentElement
+ */
+ build(parentElement) {
+ const testCaseElement = this.buildNode(this.createElement(parentElement));
+ if (this._failure) {
+ var failureElement = testCaseElement.ele('failure', this._failureAttributes);
+ if (this._stacktrace) {
+ failureElement.cdata(this._stacktrace);
+ }
+ }
+ if (this._error) {
+ var errorElement = testCaseElement.ele('error', this._errorAttributes);
+ if (this._errorContent) {
+ errorElement.cdata(this._errorContent);
+ }
+ }
+ if (this._skipped) {
+ testCaseElement.ele('skipped');
+ }
+ if (this._standardOutput) {
+ testCaseElement.ele('system-out').cdata(this._standardOutput);
}
+ var systemError;
+ if (this._standardError) {
+ systemError = testCaseElement.ele('system-err').cdata(this._standardError);
+
+ if (this._errorAttachment) {
+ systemError.txt('[[ATTACHMENT|' + this._errorAttachment + ']]');
+ }
+ }
+ return testCaseElement;
}
-};
+}
-module.exports = TestCase;
+module.exports = { TestCase: TestCase };
diff --git a/src/test_group.js b/src/test_group.js
new file mode 100755
index 0000000..a2ae6d1
--- /dev/null
+++ b/src/test_group.js
@@ -0,0 +1,111 @@
+// @ts-check
+var _ = require('lodash');
+var formatDate = require('date-format').asString;
+var { TestNode } = require('./test_node');
+
+class TestGroup extends TestNode {
+ /**
+ * @param {import('./factory').Factory} factory
+ * @param {string} elementName
+ */
+ constructor(factory, elementName) {
+ super(factory, elementName);
+ this._children = [];
+ }
+
+ /**
+ * @param {string|Date} timestamp
+ * @returns {TestGroup}
+ * @chainable
+ */
+ timestamp(timestamp) {
+ if (_.isDate(timestamp)) {
+ this._attributes.timestamp = formatDate('yyyy-MM-ddThh:mm:ss', timestamp);
+ } else {
+ this._attributes.timestamp = timestamp;
+ }
+ return this;
+ }
+
+ /**
+ * @returns {import('./test_case').TestCase}
+ */
+ testCase() {
+ var testCase = this._factory.newTestCase();
+ this._children.push(testCase);
+ return testCase;
+ }
+
+ /**
+ * @returns {number}
+ */
+ getTestCaseCount() {
+ return this._sumTestCaseCounts(function (testCase) {
+ return testCase.getTestCaseCount();
+ });
+ }
+
+ /**
+ * @returns {number}
+ */
+ getFailureCount() {
+ return this._sumTestCaseCounts(function (testCase) {
+ return testCase.getFailureCount();
+ });
+ }
+
+ /**
+ * @returns {number}
+ */
+ getErrorCount() {
+ return this._sumTestCaseCounts(function (testCase) {
+ return testCase.getErrorCount();
+ });
+ }
+
+ /**
+ * @returns {number}
+ */
+ getSkippedCount() {
+ return this._sumTestCaseCounts(function (testCase) {
+ return testCase.getSkippedCount();
+ });
+ }
+
+ /**
+ * @protected
+ * @param {Function} counterFunction
+ * @returns {number}
+ */
+ _sumTestCaseCounts(counterFunction) {
+ var counts = _.map(this._children, counterFunction);
+ return _.sum(counts);
+ }
+
+ /**
+ * @param {import('xmlbuilder').XMLElement} [parentElement]
+ * @returns {import('xmlbuilder').XMLElement}
+ */
+ build(parentElement) {
+ this._attributes.tests = this.getTestCaseCount();
+ this._attributes.failures = this.getFailureCount();
+ this._attributes.errors = this.getErrorCount();
+ this._attributes.skipped = this.getSkippedCount();
+ return super.build(parentElement);
+ }
+
+ /**
+ * @protected
+ * @param {import('xmlbuilder').XMLElement} element
+ * @returns {import('xmlbuilder').XMLElement}
+ */
+ buildNode(element) {
+ element = super.buildNode(element);
+ _.forEach(this._children, function (child) {
+ child.build(element);
+ });
+ return element;
+ }
+}
+
+module.exports = { TestGroup: TestGroup };
diff --git a/src/test_node.js b/src/test_node.js
new file mode 100644
index 0000000..7f3ba7d
--- /dev/null
+++ b/src/test_node.js
@@ -0,0 +1,126 @@
+// @ts-check
+var _ = require('lodash');
+var xmlBuilder = require('xmlbuilder');
+
+class TestNode {
+ /**
+ * @param {import('./factory').Factory} factory
+ * @param {string} elementName
+ */
+ constructor(factory, elementName) {
+ this._factory = factory;
+ this._elementName = elementName;
+ this._attributes = {};
+ this._properties = [];
+ }
+
+ /**
+ * @param {string} name
+ * @param {string} value
+ * @returns {TestNode}
+ * @chainable
+ */
+ property(name, value) {
+ this._properties.push({ name: name, value: value });
+ return this;
+ }
+
+ /**
+ * @param {string} name
+ * @returns {TestNode}
+ * @chainable
+ */
+ name(name) {
+ this._attributes.name = name;
+ return this;
+ }
+
+ /**
+ * @param {string} timeInSeconds
+ * @returns {TestNode}
+ * @chainable
+ */
+ time(timeInSeconds) {
+ this._attributes.time = timeInSeconds;
+ return this;
+ }
+
+ /**
+ * @returns {number}
+ */
+ getTestCaseCount() {
+ throw new Error('Not implemented');
+ }
+
+ /**
+ * @returns {number}
+ */
+ getFailureCount() {
+ throw new Error('Not implemented');
+ }
+
+ /**
+ * @returns {number}
+ */
+ getErrorCount() {
+ throw new Error('Not implemented');
+ }
+
+ /**
+ * @returns {number}
+ */
+ getSkippedCount() {
+ throw new Error('Not implemented');
+ }
+
+ /**
+ * @param {import('xmlbuilder').XMLElement} [parentElement]
+ */
+ build(parentElement) {
+ return this.buildNode(this.createElement(parentElement));
+ }
+
+ /**
+ * @protected
+ * @param {import('xmlbuilder').XMLElement} [parentElement]
+ * @returns {import('xmlbuilder').XMLElement}
+ */
+ createElement(parentElement) {
+ if (parentElement) {
+ return parentElement.ele(this._elementName, this._attributes);
+ }
+ return this.createRootElement();
+ }
+
+ /**
+ * @private
+ * @returns {import('xmlbuilder').XMLElement}
+ */
+ createRootElement() {
+ const element = xmlBuilder.create(this._elementName, { encoding: 'UTF-8', invalidCharReplacement: '' });
+ Object.keys(this._attributes).forEach((key) => {
+ element.att(key, this._attributes[key]);
+ });
+ return element;
+ }
+
+ /**
+ * @protected
+ * @param {import('xmlbuilder').XMLElement} element
+ * @returns {import('xmlbuilder').XMLElement}
+ */
+ buildNode(element) {
+ if (this._properties.length) {
+ var propertiesElement = element.ele('properties');
+ _.forEach(this._properties, function (property) {
+ propertiesElement.ele('property', {
+ name: property.name,
+ value: property.value,
+ });
+ });
+ }
+ return element;
+ }
+}
+
+module.exports = { TestNode: TestNode };
diff --git a/src/test_suite.js b/src/test_suite.js
index ea43f79..91e343b 100755
--- a/src/test_suite.js
+++ b/src/test_suite.js
@@ -1,86 +1,14 @@
+// @ts-check
var _ = require('lodash');
-var formatDate = require('date-format').asString;
-
-function TestSuite(factory) {
- this._factory = factory;
- this._attributes = {};
- this._testCases = [];
- this._properties = [];
-}
-
-TestSuite.prototype.name = function (name) {
- this._attributes.name = name;
- return this;
-};
-
-TestSuite.prototype.time = function (timeInSeconds) {
- this._attributes.time = timeInSeconds;
- return this;
-};
-
-TestSuite.prototype.timestamp = function (timestamp) {
- if (_.isDate(timestamp)) {
- this._attributes.timestamp = formatDate('yyyy-MM-ddThh:mm:ss', timestamp);
- } else {
- this._attributes.timestamp = timestamp;
- }
- return this;
-};
-
-TestSuite.prototype.property = function (name, value) {
- this._properties.push({ name: name, value: value });
- return this;
-};
-
-TestSuite.prototype.testCase = function () {
- var testCase = this._factory.newTestCase();
- this._testCases.push(testCase);
- return testCase;
-};
-
-TestSuite.prototype.getFailureCount = function () {
- return this._sumTestCaseCounts(function (testCase) {
- return testCase.getFailureCount();
- });
-};
-
-TestSuite.prototype.getErrorCount = function () {
- return this._sumTestCaseCounts(function (testCase) {
- return testCase.getErrorCount();
- });
-};
-
-TestSuite.prototype.getSkippedCount = function () {
- return this._sumTestCaseCounts(function (testCase) {
- return testCase.getSkippedCount();
- });
-};
-
-TestSuite.prototype._sumTestCaseCounts = function (counterFunction) {
- var counts = _.map(this._testCases, counterFunction);
- return _.sum(counts);
-};
-
-TestSuite.prototype.build = function (parentElement) {
- this._attributes.tests = this._testCases.length;
- this._attributes.failures = this.getFailureCount();
- this._attributes.errors = this.getErrorCount();
- this._attributes.skipped = this.getSkippedCount();
- var suiteElement = parentElement.ele('testsuite', this._attributes);
-
- if (this._properties.length) {
- var propertiesElement = suiteElement.ele('properties');
- _.forEach(this._properties, function (property) {
- propertiesElement.ele('property', {
- name: property.name,
- value: property.value,
- });
- });
+var { TestGroup } = require('./test_group');
+
+class TestSuite extends TestGroup {
+ /**
+ * @param {import('./factory').Factory} factory
+ */
+ constructor(factory) {
+ super(factory, 'testsuite');
}
+}
- _.forEach(this._testCases, function (testCase) {
- testCase.build(suiteElement);
- });
-};
-
-module.exports = TestSuite;
+module.exports = { TestSuite: TestSuite };
diff --git a/src/test_suites.js b/src/test_suites.js
new file mode 100755
index 0000000..a32fba1
--- /dev/null
+++ b/src/test_suites.js
@@ -0,0 +1,22 @@
+// @ts-check
+var { TestGroup } = require('./test_group');
+
+class TestSuites extends TestGroup {
+ /**
+ * @param {import('./factory').Factory} factory
+ */
+ constructor(factory) {
+ super(factory, 'testsuites');
+ }
+
+ /**
+ * @returns {import('./test_suite').TestSuite}
+ */
+ testSuite() {
+ var suite = this._factory.newTestSuite();
+ this._children.push(suite);
+ return suite;
+ }
+}
+
+module.exports = { TestSuites: TestSuites };