diff --git a/README.md b/README.md
index 38f592e8..3e76d22e 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Browser tests using [puppeteer](https://pptr.dev/) benefit from special support
Depending on the environment (you can set up configurations to run the same tests against dev, stage, prod etc.), tests can be skipped, or marked as _expected to fail_ for test driven development where you write tests first before fixing a bug or implementing a feature.
A locking system prevents two tests or the same tests on two different machines from accessing a shared resource, e.g. a test account.
-You can review test results in a PDF report.
+You can review test results in a PDF report or create a JUnit report file to be consumed by CI systems.
## Installation
@@ -391,6 +391,9 @@ The keys are up to you; for example you probably want to have a main entry point
-J, --json Write tests results as a JSON file.
--json-file FILE.json
JSON file to write to. Defaults to results.json .
+--junit Write tests results as a JUnit XML file.
+--junit-file FILE.xml
+ JUnit XML file to write to. Defaults to results.xml .
-H, --html Write test results as an HTML file.
--html-file FILE.html
HTML file to write a report to. Defaults to results.html .
diff --git a/src/config.js b/src/config.js
index 878c13c0..17711bdd 100644
--- a/src/config.js
+++ b/src/config.js
@@ -91,6 +91,7 @@ function computeConcurrency(spec, { cpuCount = undefined } = {}) {
function parseArgs(options, raw_args) {
const DEFAULT_HTML_NAME = 'results.html';
const DEFAULT_JSON_NAME = 'results.json';
+ const DEFAULT_JUNIT_NAME = 'results.xml';
const DEFAULT_MARKDOWN_NAME = 'results.md';
const DEFAULT_PDF_NAME = 'results.pdf';
@@ -184,6 +185,16 @@ function parseArgs(options, raw_args) {
defaultValue: DEFAULT_JSON_NAME,
help: 'JSON file to write to. Defaults to %(defaultValue)s .',
});
+ results_group.addArgument(['--junit'], {
+ action: 'storeTrue',
+ help: 'Write tests results as a JUnit XML file.',
+ });
+ results_group.addArgument(['--junit-file'], {
+ metavar: 'FILE.xml',
+ dest: 'junit_file',
+ defaultValue: DEFAULT_JUNIT_NAME,
+ help: 'JUnit XML file to write to. Defaults to %(defaultValue)s .',
+ });
results_group.addArgument(['-H', '--html'], {
action: 'storeTrue',
help: 'Write test results as an HTML file.',
diff --git a/src/render.js b/src/render.js
index e2619b22..dd7d08f0 100644
--- a/src/render.js
+++ b/src/render.js
@@ -88,7 +88,7 @@ function craftResults(config, test_info) {
async function doRender(config, results) {
output.logVerbose(
config,
- `[results] Render results JSON: ${config.json}, Markdown: ${config.markdown}, HTML: ${config.html}, PDF: ${config.pdf}`
+ `[results] Render results JSON: ${config.json}, JUnit: ${config.junit}, Markdown: ${config.markdown}, HTML: ${config.html}, PDF: ${config.pdf}`
);
if (config.json) {
@@ -125,6 +125,14 @@ async function doRender(config, results) {
output.logVerbose(config, `Rendering to PDF ${config.pdf_file}...`);
await pdf(config, config.pdf_file, results);
}
+
+ if (config.junit) {
+ output.logVerbose(config, `Rendering to JUnit ${config.junit_file}...`);
+ const junit_xml = junit(results);
+ await fs.promises.writeFile(config.junit_file, junit_xml, {
+ encoding: 'utf-8',
+ });
+ }
}
function format_duration(ms) {
@@ -650,6 +658,48 @@ async function pdf(config, path, results) {
return html2pdf(config, path, html(results));
}
+/**
+ * @param {import('./internal').CraftedResults} results
+ */
+function junit(results) {
+ const testcases = results.tests.map(testResult => {
+ const { skipped, taskResults } = testResult;
+
+ // Just take the first error if there is any, on retries it would probably be the same one
+ const error = taskResults.find(tr => tr.status === 'error');
+
+ const res =
+ `` +
+ (skipped ? `${testResult.skipReason}` : '') +
+ (error
+ ? `${
+ escape_html(error.error_stack) ||
+ 'INTERNAL ERROR: no error stack'
+ }`
+ : '') +
+ (testResult.description
+ ? `${escape_html(
+ testResult.description
+ )}`
+ : '') +
+ ``;
+ return res;
+ });
+
+ const result =
+ '\n' +
+ '\n' +
+ '\n' +
+ testcases.join('\n') +
+ '\n' +
+ '\n' +
+ '';
+
+ return result;
+}
+
module.exports = {
craftResults,
doRender,
diff --git a/tests/selftest_flaky_result_repeat.js b/tests/selftest_flaky_result_repeat.js
index a04cea0c..f90572f6 100644
--- a/tests/selftest_flaky_result_repeat.js
+++ b/tests/selftest_flaky_result_repeat.js
@@ -45,13 +45,21 @@ async function run() {
}
}
- const summary = lines.slice(0, 3).map(s => s.trim());
- assert.deepEqual(summary, [
+ const summary = lines.map(s => s.trim());
+ const expected = [
'3 tests passed',
'3 failed (error[0], error[1], error[2])',
'3 flaky (flaky[0], flaky[1], flaky[2])',
- ]);
- assert.match(lines[3], /3 slowest tests: (.+), (.+), (.+)/);
+ ];
+ for (const item of expected) {
+ assert(
+ summary.includes(item),
+ `Expected test summary to include: ${item}\nActual summary: ${JSON.stringify(
+ summary
+ )}`
+ );
+ }
+ assert.match(lines[lines.length - 1], /3 slowest tests: (.+), (.+), (.+)/);
}
module.exports = {