1 |
|
2 | var jasmineReporters = require("../index");
|
3 | var DOMParser = require("@xmldom/xmldom").DOMParser;
|
4 |
|
5 | var env, suite,
|
6 | reporter, writeCalls, suiteId=0, specId=0, noop=function(){};
|
7 | function 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 | }
|
17 | function 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")
|
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 |
|
35 | function setupReporterWithOptions(options) {
|
36 | reporter = new jasmineReporters.JUnitXmlReporter(options);
|
37 | reporter.writeFile = jasmine.createSpy();
|
38 | }
|
39 |
|
40 |
|
41 | function 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 |
|
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 | }
|
59 | function 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 |
|
74 | function xmlDocumentFromString(str) {
|
75 | return (new DOMParser()).parseFromString(str, "text/xml");
|
76 | }
|
77 |
|
78 | describe("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 |
|
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 < & > " ' | : \\ /");
|
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 <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("& < > " '");
|
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 |
|
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 |
|
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 | });
|