UNPKG

23.9 kBJavaScriptView Raw
1/* globals jasmine, describe, afterEach, beforeEach, it, expect */
2var jasmineReporters = require("../index");
3var DOMParser = require("@xmldom/xmldom").DOMParser;
4
5var env, suite,
6 reporter, writeCalls, suiteId=0, specId=0, noop=function(){};
7function fakeSpec(ste, name) {
8 var s = new jasmine.Spec({
9 env: env,
10 id: specId++,
11 description: name,
12 queueableFn: {fn: noop},
13 });
14 ste.addChild(s);
15 return s;
16}
17function fakeSuite(name, parentSuite) {
18 var s = new jasmine.Suite({
19 env: env,
20 id: suiteId++,
21 description: name,
22 parentSuite: parentSuite || jasmine.createSpy("pretend top suite") // I'm sure there's a good reason Jasmine does this...
23 });
24 if (parentSuite) {
25 parentSuite.addChild(s);
26 }
27 else {
28
29 env._suites = env._suites || [];
30 env._suites.push(s);
31 }
32 return s;
33}
34
35function setupReporterWithOptions(options) {
36 reporter = new jasmineReporters.JUnitXmlReporter(options);
37 reporter.writeFile = jasmine.createSpy();
38}
39
40// make sure reporter is set before calling this
41function triggerRunnerEvents(callback) {
42 reporter.jasmineStarted();
43 for (var i=0; i<env._suites.length; i++) {
44 var s = env._suites[i];
45 if(callback && typeof(callback) === "function") {
46 callback();
47 }
48 triggerSuiteEvents(s);
49 }
50 reporter.jasmineDone();
51
52 // pre-parse some data to be used by various specs
53 writeCalls = reporter.writeFile.calls.all();
54 for (i=0; i<writeCalls.length; i++) {
55 writeCalls[i].output = writeCalls[i].args[1];
56 writeCalls[i].xmldoc = xmlDocumentFromString(writeCalls[i].output);
57 }
58}
59function triggerSuiteEvents(ste) {
60 reporter.suiteStarted(ste.result);
61 var thing;
62 for (var i=0; i<ste.children.length; i++) {
63 thing = ste.children[i];
64 if (thing instanceof jasmine.Suite) {
65 triggerSuiteEvents(thing);
66 } else {
67 reporter.specStarted(thing.result);
68 reporter.specDone(thing.result);
69 }
70 }
71 reporter.suiteDone(ste.result);
72}
73
74function xmlDocumentFromString(str) {
75 return (new DOMParser()).parseFromString(str, "text/xml");
76}
77
78describe("JUnitXmlReporter", function(){
79 beforeEach(function(){
80 env = new jasmine.Env();
81 suite = fakeSuite("ParentSuite");
82 fakeSpec(suite, "should be a dummy with invalid characters: & < > \" '");
83 setupReporterWithOptions();
84 });
85
86 describe("constructor", function(){
87 it("should default path to an empty string", function(){
88 expect(reporter.savePath).toEqual("");
89 });
90 it("should default consolidateAll to true", function(){
91 expect(reporter.consolidateAll).toBe(true);
92 });
93 it("should default consolidate to true", function(){
94 expect(reporter.consolidate).toBe(true);
95 });
96 it("should default useDotNotation to true", function(){
97 expect(reporter.useDotNotation).toBe(true);
98 });
99
100 describe("file prefix", function(){
101 it("should default output file prefix to 'junitresults'", function () {
102 expect(reporter.filePrefix).toBe("junitresults");
103 });
104 it("should default output file prefix to 'junitresults-' if consolidateAll is false", function () {
105 setupReporterWithOptions({
106 consolidateAll: false
107 });
108 expect(reporter.filePrefix).toBe("junitresults-");
109 });
110 it("should prefix suite names if consolidateAll is false", function () {
111 setupReporterWithOptions({
112 consolidateAll: false,
113 filePrefix: "alt-prefix-"
114 });
115 triggerRunnerEvents();
116 expect(reporter.writeFile).toHaveBeenCalledWith("alt-prefix-ParentSuite.xml", jasmine.any(String));
117 });
118 });
119
120 describe("package", function () {
121 it("should default output package to undefined", function () {
122 expect(reporter.package).toBeUndefined();
123 });
124 it("should not set output package if a non-string is provided", function() {
125 setupReporterWithOptions({package:true});
126 expect(reporter.package).toBeUndefined();
127
128 setupReporterWithOptions({package:["test"]});
129 expect(reporter.package).toBeUndefined();
130 });
131 it("should set output package to the provided string", function () {
132 setupReporterWithOptions({package:"testPackage"});
133 expect(reporter.package).toBe("testPackage");
134 });
135 });
136
137 describe("stylesheetPath", function () {
138 it("should default stylesheetPath to undefined", function () {
139 expect(reporter.stylesheetPath).toBeUndefined();
140 });
141 it("should not set stylesheetPath if an empty or non-string is provided", function() {
142 setupReporterWithOptions({stylesheetPath:true});
143 expect(reporter.stylesheetPath).toBeUndefined();
144
145 setupReporterWithOptions({stylesheetPath:""});
146 expect(reporter.stylesheetPath).toBeUndefined();
147 });
148 it("should set output stylesheetPath to the provided string", function () {
149 setupReporterWithOptions({stylesheetPath:"mystyle.xslt"});
150 expect(reporter.stylesheetPath).toBe("mystyle.xslt");
151 });
152 it("should include the stylesheet in all generated output files", function () {
153 setupReporterWithOptions({consolidate: false, stylesheetPath:"mystyle.xslt"});
154 triggerRunnerEvents();
155 writeCalls.forEach(call => {
156 expect(call.output.indexOf('<?xml-stylesheet type="text/xsl" href="mystyle.xslt"')).toBeGreaterThan(-1);
157 });
158 });
159 });
160 });
161
162 function assertTestsuitesTagAttributes(testSuitesTag, {disabled, errors, failures, tests} = {}) {
163 if (disabled === void 0) {
164 expect(testSuitesTag.hasAttribute("disabled")).toBe(false);
165 } else {
166 expect(testSuitesTag.getAttribute("disabled")).toBe(disabled);
167 }
168 expect(testSuitesTag.getAttribute("errors")).toBe(errors);
169 expect(testSuitesTag.getAttribute("failures")).toBe(failures);
170 expect(testSuitesTag.getAttribute("tests")).toBe(tests);
171 }
172
173
174 it("the testsuites tags should include a time attribute", function() {
175 var testSuitesTags = writeCalls[0].xmldoc.getElementsByTagName("testsuites");
176 expect(testSuitesTags.length).toBe(1);
177 var testSuitesTag = testSuitesTags[0];
178 expect(testSuitesTag.getAttribute("time")).not.toBe("");
179 });
180
181 describe("no xml output generation", function() {
182 beforeEach(function() {
183 setupReporterWithOptions({consolidateAll:true});
184 triggerRunnerEvents();
185 });
186
187 it("testsuites tags should default disabled, errors, failures to 0 when undefined", function() {
188 assertTestsuitesTagAttributes(
189 writeCalls[0].xmldoc.getElementsByTagName("testsuites")[0],
190 {disabled: "0", errors: "0", failures: "0", tests: "1"});
191 });
192 });
193
194 describe("generated xml output", function(){
195 var subSuite, subSubSuite, siblingSuite;
196 function itShouldIncludeXmlPreambleInAllFiles() {
197 it("should include xml preamble once in all files", function() {
198 for (var i=0; i<writeCalls.length; i++) {
199 expect(writeCalls[i].output.indexOf("<?xml")).toBe(0);
200 expect(writeCalls[i].output.lastIndexOf("<?xml")).toBe(0);
201 }
202 });
203 }
204 function itShouldHaveOneTestsuitesElementPerFile() {
205 it("should include xml preamble once in all files", function() {
206 for (var i=0; i<writeCalls.length; i++) {
207 expect(writeCalls[i].xmldoc.getElementsByTagName("testsuites").length).toBe(1);
208 }
209 });
210 }
211
212 beforeEach(function(){
213 subSuite = fakeSuite("SubSuite", suite);
214 subSubSuite = fakeSuite("SubSubSuite", subSuite);
215 siblingSuite = fakeSuite("SiblingSuite With Invalid Chars < & > \" ' | : \\ /");
216 fakeSpec(subSuite, "should be one level down");
217 fakeSpec(subSubSuite, "should be two levels down");
218 var skipped = fakeSpec(subSubSuite, "should be skipped two levels down");
219 var disabled = fakeSpec(subSubSuite, "should be disabled two levels down");
220 var failed = fakeSpec(subSubSuite, "should be failed two levels down");
221 fakeSpec(siblingSuite, "should be a sibling of Parent");
222 skipped.result.status = "pending";
223 disabled.result.status = "disabled";
224 failed.result.status = "failed";
225 failed.result.failedExpectations.push({
226 passed: false,
227 message: "Expected true to be false.",
228 expected: false,
229 actual: true,
230 matcherName: "toBe",
231 stack: 'Stack trace! Stack trackes are cool & can have "special" characters <3\n\n Neat: yes.'
232 });
233 });
234
235 describe("consolidateAll=true", function() {
236 beforeEach(function() {
237 setupReporterWithOptions({consolidateAll:true, filePrefix:"results"});
238 triggerRunnerEvents();
239 });
240 it("should only write a single file", function() {
241 expect(writeCalls.length).toBe(1);
242 });
243 it("should include results for all test suites", function() {
244 expect(writeCalls[0].xmldoc.getElementsByTagName("testsuite").length).toBe(4);
245 });
246 it("should write a single file using filePrefix as the filename", function() {
247 expect(writeCalls[0].args[0]).toBe("results.xml");
248 });
249 it("testsuites tags should include disabled, errors, failures, and tests (count) when defined", function() {
250 assertTestsuitesTagAttributes(
251 writeCalls[0].xmldoc.getElementsByTagName("testsuites")[0],
252 {disabled: "1", errors: "0", failures: "1", tests: "7"});
253 });
254 itShouldHaveOneTestsuitesElementPerFile();
255 itShouldIncludeXmlPreambleInAllFiles();
256 });
257 describe("consolidatedAll=false, consolidate=true", function(){
258 beforeEach(function(){
259 setupReporterWithOptions({consolidateAll:false, consolidate:true, filePrefix:"results-"});
260 triggerRunnerEvents();
261 });
262 it("should write one file per parent suite", function(){
263 expect(writeCalls.length).toEqual(2);
264 });
265 it("should include results for top-level suite and its descendents", function() {
266 expect(writeCalls[0].xmldoc.getElementsByTagName("testsuite").length).toBe(3);
267 expect(writeCalls[1].xmldoc.getElementsByTagName("testsuite").length).toBe(1);
268 });
269 it("should construct filenames using filePrefix and suite description, removing bad characters", function() {
270 expect(writeCalls[0].args[0]).toBe("results-ParentSuite.xml");
271 expect(writeCalls[1].args[0]).toBe("results-SiblingSuiteWithInvalidChars.xml");
272 });
273 it("testsuites tags should include disabled, errors, failures, and tests (count) when defined", function() {
274 assertTestsuitesTagAttributes(writeCalls[0].xmldoc.getElementsByTagName("testsuites")[0],
275 {disabled: "1", errors: "0", failures: "1", tests: "6"});
276 assertTestsuitesTagAttributes(writeCalls[1].xmldoc.getElementsByTagName("testsuites")[0],
277 {disabled: "0", errors: "0", failures: "0", tests: "1"});
278 });
279 itShouldHaveOneTestsuitesElementPerFile();
280 itShouldIncludeXmlPreambleInAllFiles();
281 });
282 describe("consolidated=false", function(){
283 beforeEach(function(){
284 // consolidateAll becomes a noop, we include it specifically to passively test that
285 setupReporterWithOptions({consolidateAll:true, consolidate:false, filePrefix:"results-"});
286 triggerRunnerEvents();
287 });
288 it("should write one file per suite", function(){
289 expect(writeCalls.length).toEqual(4);
290 });
291 it("should include results for a single suite", function() {
292 for (var i=0; i<writeCalls.length; i++) {
293 expect(writeCalls[i].xmldoc.getElementsByTagName("testsuite").length).toBe(1);
294 }
295 });
296 it("should construct filenames using filePrefix and suite description, always using dot notation for filenames", function() {
297 expect(writeCalls[0].args[0]).toBe("results-ParentSuite.SubSuite.SubSubSuite.xml");
298 expect(writeCalls[1].args[0]).toBe("results-ParentSuite.SubSuite.xml");
299 expect(writeCalls[2].args[0]).toBe("results-ParentSuite.xml");
300 });
301 itShouldHaveOneTestsuitesElementPerFile();
302 itShouldIncludeXmlPreambleInAllFiles();
303 });
304
305 describe("classname generation", function() {
306 it("should remove invalid xml chars from the classname", function() {
307 setupReporterWithOptions({consolidateAll:true, consolidate:true});
308 triggerRunnerEvents();
309 expect(writeCalls[0].output).toContain("SiblingSuite With Invalid Chars &lt; &amp; &gt; &quot; &apos; | : \\ /");
310 });
311 describe("useDotNotation=true", function() {
312 beforeEach(function() {
313 setupReporterWithOptions({consolidateAll:true, consolidate:true, useDotNotation:true});
314 triggerRunnerEvents();
315 });
316 it("should use suite descriptions separated by periods", function() {
317 expect(writeCalls[0].xmldoc.getElementsByTagName("testsuite")[2].getAttribute("name")).toBe("ParentSuite.SubSuite.SubSubSuite");
318 expect(writeCalls[0].xmldoc.getElementsByTagName("testcase")[2].getAttribute("classname")).toBe("ParentSuite.SubSuite.SubSubSuite");
319 });
320 });
321 describe("useDotNotation=false", function() {
322 beforeEach(function() {
323 setupReporterWithOptions({consolidateAll:true, consolidate:true, useDotNotation:false});
324 triggerRunnerEvents();
325 });
326 it("should use suite descriptions separated by spaces", function() {
327 expect(writeCalls[0].xmldoc.getElementsByTagName("testsuite")[2].getAttribute("name")).toBe("ParentSuite SubSuite SubSubSuite");
328 expect(writeCalls[0].xmldoc.getElementsByTagName("testcase")[2].getAttribute("classname")).toBe("ParentSuite SubSuite SubSubSuite");
329 });
330 });
331 });
332
333 describe("suite result generation", function() {
334 var suites;
335 beforeEach(function() {
336 setupReporterWithOptions({consolidateAll:true, consolidate:true});
337 triggerRunnerEvents();
338 suites = writeCalls[0].xmldoc.getElementsByTagName("testsuite");
339 });
340 it("should include test suites in order", function() {
341 expect(suites[0].getAttribute("name")).toBe("ParentSuite");
342 expect(suites[1].getAttribute("name")).toContain("SubSuite");
343 expect(suites[2].getAttribute("name")).toContain("SubSubSuite");
344 expect(suites[3].getAttribute("name")).toContain("SiblingSuite");
345 });
346 it("should include total / failed / skipped counts for each suite (ignoring descendent results)", function() {
347 expect(suites[1].getAttribute("tests")).toBe("1");
348 expect(suites[2].getAttribute("tests")).toBe("4");
349 expect(suites[2].getAttribute("skipped")).toBe("1");
350 expect(suites[2].getAttribute("failures")).toBe("1");
351 });
352 it("should calculate duration", function() {
353 expect(Number(suites[0].getAttribute("time"))).not.toEqual(NaN);
354 });
355 it("should include timestamp as an ISO date string without timezone", function() {
356 expect(suites[0].getAttribute("timestamp")).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
357 });
358 it("should include hostname, simply because the JUnit XSD says it is required", function() {
359 expect(suites[0].getAttribute("hostname")).toBe("localhost");
360 });
361 describe("package", function() {
362 it("should not include the package attribute if it is not provided", function() {
363 setupReporterWithOptions({});
364 triggerRunnerEvents();
365 suites = writeCalls[0].xmldoc.getElementsByTagName("testsuite");
366 expect(suites[0].getAttribute("package")).toBe("");
367 });
368 it("should include the package attribute if a string is provided", function() {
369 setupReporterWithOptions({package:"testPackage"});
370 triggerRunnerEvents();
371 suites = writeCalls[0].xmldoc.getElementsByTagName("testsuite");
372 expect(suites[0].getAttribute("package")).toBe("testPackage");
373 });
374 it("should escape the string provided", function() {
375 setupReporterWithOptions({package:"testPackage <3"});
376 triggerRunnerEvents();
377 suites = writeCalls[0].xmldoc.getElementsByTagName("testsuite");
378 expect(suites[0].getAttribute("package")).toBe("testPackage <3");
379 expect(writeCalls[0].output).toContain('package="testPackage &lt;3"');
380 });
381 });
382 });
383
384 describe("spec result generation", function() {
385 var specs;
386 beforeEach(function() {
387 setupReporterWithOptions({consolidateAll:true, consolidate:true});
388 triggerRunnerEvents();
389 specs = writeCalls[0].xmldoc.getElementsByTagName("testcase");
390 });
391 it("should include specs in order", function() {
392 expect(specs[0].getAttribute("name")).toContain("should be a dummy");
393 expect(specs[5].getAttribute("name")).toBe("should be failed two levels down");
394 });
395 it("should escape bad xml characters in spec description", function() {
396 expect(writeCalls[0].output).toContain("&amp; &lt; &gt; &quot; &apos;");
397 });
398 it("should calculate duration", function() {
399 expect(Number(specs[0].getAttribute("time"))).not.toEqual(NaN);
400 });
401 it("should include failed matcher name as the failure type", function() {
402 var failure = specs[5].getElementsByTagName("failure")[0];
403 expect(failure.getAttribute("type")).toBe("toBe");
404 });
405 it("should include failure messages", function() {
406 var failure = specs[5].getElementsByTagName("failure")[0];
407 expect(failure.getAttribute("message")).toBe("Expected true to be false.");
408 });
409 it("should include stack traces for failed specs (using CDATA to preserve special characters)", function() {
410 var failure = specs[5].getElementsByTagName("failure")[0];
411 expect(failure.textContent).toContain('cool & can have "special" characters <3');
412 });
413 it("should include <skipped/> for skipped specs", function() {
414 expect(specs[3].getElementsByTagName("skipped").length).toBe(1);
415 });
416 });
417
418 describe("modifySuiteName", function(){
419 var suites, modification = "-modified";
420 beforeEach(function(){
421 // consolidateAll becomes a noop, we include it specifically to passively test that
422 setupReporterWithOptions({
423 consolidateAll:true,
424 consolidate:true,
425 modifySuiteName:function(generatedName/*, suite*/) {
426 return generatedName + modification;
427 }
428 });
429 triggerRunnerEvents();
430 suites = writeCalls[0].xmldoc.getElementsByTagName("testsuite");
431 });
432 it("should construct suitenames that contain modification", function() {
433 for (var i = 0, suite; i < suites.length; i++) {
434 suite = suites[i];
435 expect(suite.getAttribute("name")).toContain(modification);
436 }
437 });
438 itShouldHaveOneTestsuitesElementPerFile();
439 itShouldIncludeXmlPreambleInAllFiles();
440 });
441
442 describe("modifyReportFileName", function(){
443 var modification = "-modified";
444 beforeEach(function(){
445 // consolidateAll becomes a noop, we include it specifically to passively test that
446 setupReporterWithOptions({
447 consolidateAll:false,
448 modifyReportFileName:function(generatedName/*, suite*/) {
449 return generatedName + modification;
450 }
451 });
452 triggerRunnerEvents();
453 });
454 it("should construct filenames that contain modification", function() {
455 expect(writeCalls[0].args[0]).toContain(modification);
456 });
457 itShouldHaveOneTestsuitesElementPerFile();
458 itShouldIncludeXmlPreambleInAllFiles();
459 });
460
461 describe("suppressDisabled=true", function() {
462 beforeEach(function() {
463 setupReporterWithOptions({suppressDisabled: true});
464 triggerRunnerEvents();
465 });
466 it("testsuites tags should include errors, failures, and tests (count) when defined, but not disabled", function() {
467 assertTestsuitesTagAttributes(
468 writeCalls[0].xmldoc.getElementsByTagName("testsuites")[0],
469 {disabled: void 0, errors: "0", failures: "1", tests: "7"});
470 });
471 });
472
473 describe("captures stdout in <xml-output>", function(){
474 var specOutputs;
475 const testOutput = "I'm generating test output.";
476 var _stdoutWrite;
477 beforeEach(function(){
478 _stdoutWrite = process.stdout.write;
479 process.stdout.write = noop;
480 setupReporterWithOptions( {consolidateAll:true, consolidate:true, captureStdout: true});
481 triggerRunnerEvents(function() {
482 console.log(testOutput);
483 });
484 specOutputs = writeCalls[0].xmldoc.getElementsByTagName("system-out");
485 });
486 afterEach(function() {
487 process.stdout.write = _stdoutWrite;
488 });
489 it("should record stdout", function() {
490 expect(specOutputs[0].textContent).toContain(testOutput);
491 });
492 it("should discard any stdout for skipped tests", function() {
493 expect(specOutputs[3].textContent).not.toContain(testOutput);
494 });
495 });
496 });
497});