Skip to content

Commit dc238e9

Browse files
jpleclercJean-Philippe LeclercJean-Philippe Leclerchi-ogawa
authored
feat(reporter): add support for function type to classname option in the junit reporter (#6839)
Co-authored-by: Jean-Philippe Leclerc <[email protected]> Co-authored-by: Jean-Philippe Leclerc <[email protected]> Co-authored-by: Hiroshi Ogawa <[email protected]>
1 parent e04a136 commit dc238e9

File tree

5 files changed

+170
-5
lines changed

5 files changed

+170
-5
lines changed

docs/guide/reporters.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,13 +290,17 @@ AssertionError: expected 5 to be 4 // Object.is equality
290290
</testsuites>
291291
```
292292

293-
The outputted XML contains nested `testsuites` and `testcase` tags. You can use the reporter options to configure these attributes:
293+
The outputted XML contains nested `testsuites` and `testcase` tags. These can also be customized via reporter options `suiteName` and `classnameTemplate`. `classnameTemplate` can either be a template string or a function.
294+
295+
The supported placeholders for the `classnameTemplate` option are:
296+
- filename
297+
- filepath
294298

295299
```ts
296300
export default defineConfig({
297301
test: {
298302
reporters: [
299-
['junit', { suiteName: 'custom suite name', classname: 'custom-classname' }]
303+
['junit', { suiteName: 'custom suite name', classnameTemplate: 'filename:{filename} - filepath:{filepath}' }]
300304
]
301305
},
302306
})

packages/vitest/src/node/reporters/junit.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,20 @@ import { getOutputFile } from '../../utils/config-helpers'
1111
import { capturePrintError } from '../error'
1212
import { IndentedLogger } from './renderers/indented-logger'
1313

14+
interface ClassnameTemplateVariables {
15+
filename: string
16+
filepath: string
17+
}
18+
1419
export interface JUnitOptions {
1520
outputFile?: string
21+
/** @deprecated Use `classnameTemplate` instead. */
1622
classname?: string
23+
24+
/**
25+
* Template for the classname attribute. Can be either a string or a function. The string can contain placeholders {filename} and {filepath}.
26+
*/
27+
classnameTemplate?: string | ((classnameVariables: ClassnameTemplateVariables) => string)
1728
suiteName?: string
1829
/**
1930
* Write <system-out> and <system-err> for console output
@@ -195,10 +206,29 @@ export class JUnitReporter implements Reporter {
195206

196207
async writeTasks(tasks: Task[], filename: string): Promise<void> {
197208
for (const task of tasks) {
209+
let classname = filename
210+
211+
const templateVars: ClassnameTemplateVariables = {
212+
filename: task.file.name,
213+
filepath: task.file.filepath,
214+
}
215+
216+
if (typeof this.options.classnameTemplate === 'function') {
217+
classname = this.options.classnameTemplate(templateVars)
218+
}
219+
else if (typeof this.options.classnameTemplate === 'string') {
220+
classname = this.options.classnameTemplate
221+
.replace(/\{filename\}/g, templateVars.filename)
222+
.replace(/\{filepath\}/g, templateVars.filepath)
223+
}
224+
else if (typeof this.options.classname === 'string') {
225+
classname = this.options.classname
226+
}
227+
198228
await this.writeElement(
199229
'testcase',
200230
{
201-
classname: this.options.classname ?? filename,
231+
classname,
202232
file: this.options.addFileAttribute ? filename : undefined,
203233
name: task.name,
204234
time: getDuration(task),

test/reporters/src/data.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,37 @@ const suite: Suite = {
2525
tasks: [],
2626
}
2727

28+
const passedFile: File = {
29+
id: '1223128da3',
30+
name: 'basic.test.ts',
31+
type: 'suite',
32+
suite,
33+
meta: {},
34+
mode: 'run',
35+
filepath: '/vitest/test/core/test/basic.test.ts',
36+
result: { state: 'pass', duration: 145.99284195899963 },
37+
tasks: [
38+
],
39+
projectName: '',
40+
file: null!,
41+
}
42+
passedFile.file = passedFile
43+
passedFile.tasks.push({
44+
id: '1223128da3_0_0',
45+
type: 'test',
46+
name: 'Math.sqrt()',
47+
mode: 'run',
48+
fails: undefined,
49+
suite,
50+
meta: {},
51+
file: passedFile,
52+
result: {
53+
state: 'pass',
54+
duration: 1.4422860145568848,
55+
},
56+
context: null as any,
57+
})
58+
2859
const error: ErrorWithDiff = {
2960
name: 'AssertionError',
3061
message: 'expected 2.23606797749979 to equal 2',
@@ -176,5 +207,6 @@ file.tasks = [suite]
176207
suite.tasks = tasks
177208

178209
const files = [file]
210+
const passedFiles = [passedFile]
179211

180-
export { files }
212+
export { files, passedFiles }

test/reporters/tests/__snapshots__/reporters.spec.ts.snap

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,39 @@ exports[`JUnit reporter 1`] = `
1414
"
1515
`;
1616

17+
exports[`JUnit reporter with custom function classnameTemplate 1`] = `
18+
"<?xml version="1.0" encoding="UTF-8" ?>
19+
<testsuites name="vitest tests" tests="1" failures="0" errors="0" time="0">
20+
<testsuite name="test/core/test/basic.test.ts" timestamp="2022-01-19T10:10:01.759Z" hostname="hostname" tests="1" failures="0" errors="0" skipped="0" time="0.145992842">
21+
<testcase classname="filename:basic.test.ts - filepath:/vitest/test/core/test/basic.test.ts" name="Math.sqrt()" time="0.001442286">
22+
</testcase>
23+
</testsuite>
24+
</testsuites>
25+
"
26+
`;
27+
28+
exports[`JUnit reporter with custom string classname 1`] = `
29+
"<?xml version="1.0" encoding="UTF-8" ?>
30+
<testsuites name="vitest tests" tests="1" failures="0" errors="0" time="0">
31+
<testsuite name="test/core/test/basic.test.ts" timestamp="2022-01-19T10:10:01.759Z" hostname="hostname" tests="1" failures="0" errors="0" skipped="0" time="0.145992842">
32+
<testcase classname="my-custom-classname" name="Math.sqrt()" time="0.001442286">
33+
</testcase>
34+
</testsuite>
35+
</testsuites>
36+
"
37+
`;
38+
39+
exports[`JUnit reporter with custom string classnameTemplate 1`] = `
40+
"<?xml version="1.0" encoding="UTF-8" ?>
41+
<testsuites name="vitest tests" tests="1" failures="0" errors="0" time="0">
42+
<testsuite name="test/core/test/basic.test.ts" timestamp="2022-01-19T10:10:01.759Z" hostname="hostname" tests="1" failures="0" errors="0" skipped="0" time="0.145992842">
43+
<testcase classname="filename:basic.test.ts - filepath:/vitest/test/core/test/basic.test.ts" name="Math.sqrt()" time="0.001442286">
44+
</testcase>
45+
</testsuite>
46+
</testsuites>
47+
"
48+
`;
49+
1750
exports[`JUnit reporter with outputFile 1`] = `
1851
"JUNIT report written to <process-cwd>/report.xml
1952
"
@@ -62,6 +95,17 @@ exports[`JUnit reporter with outputFile object in non-existing directory 2`] = `
6295
"
6396
`;
6497
98+
exports[`JUnit reporter without classname 1`] = `
99+
"<?xml version="1.0" encoding="UTF-8" ?>
100+
<testsuites name="vitest tests" tests="1" failures="0" errors="0" time="0">
101+
<testsuite name="test/core/test/basic.test.ts" timestamp="2022-01-19T10:10:01.759Z" hostname="hostname" tests="1" failures="0" errors="0" skipped="0" time="0.145992842">
102+
<testcase classname="test/core/test/basic.test.ts" name="Math.sqrt()" time="0.001442286">
103+
</testcase>
104+
</testsuite>
105+
</testsuites>
106+
"
107+
`;
108+
65109
exports[`json reporter (no outputFile entry) 1`] = `
66110
{
67111
"numFailedTestSuites": 1,

test/reporters/tests/reporters.spec.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { JUnitReporter } from '../../../packages/vitest/src/node/reporters/junit
66
import { TapReporter } from '../../../packages/vitest/src/node/reporters/tap'
77
import { TapFlatReporter } from '../../../packages/vitest/src/node/reporters/tap-flat'
88
import { getContext } from '../src/context'
9-
import { files } from '../src/data'
9+
import { files, passedFiles } from '../src/data'
1010

1111
const beautify = (json: string) => JSON.parse(json)
1212

@@ -60,6 +60,61 @@ test('JUnit reporter', async () => {
6060
expect(context.output).toMatchSnapshot()
6161
})
6262

63+
test('JUnit reporter without classname', async () => {
64+
// Arrange
65+
const reporter = new JUnitReporter({})
66+
const context = getContext()
67+
68+
// Act
69+
await reporter.onInit(context.vitest)
70+
71+
await reporter.onFinished(passedFiles)
72+
73+
// Assert
74+
expect(context.output).toMatchSnapshot()
75+
})
76+
77+
test('JUnit reporter with custom string classname', async () => {
78+
// Arrange
79+
const reporter = new JUnitReporter({ classname: 'my-custom-classname' })
80+
const context = getContext()
81+
82+
// Act
83+
await reporter.onInit(context.vitest)
84+
85+
await reporter.onFinished(passedFiles)
86+
87+
// Assert
88+
expect(context.output).toMatchSnapshot()
89+
})
90+
91+
test('JUnit reporter with custom function classnameTemplate', async () => {
92+
// Arrange
93+
const reporter = new JUnitReporter({ classnameTemplate: task => `filename:${task.filename} - filepath:${task.filepath}` })
94+
const context = getContext()
95+
96+
// Act
97+
await reporter.onInit(context.vitest)
98+
99+
await reporter.onFinished(passedFiles)
100+
101+
// Assert
102+
expect(context.output).toMatchSnapshot()
103+
})
104+
test('JUnit reporter with custom string classnameTemplate', async () => {
105+
// Arrange
106+
const reporter = new JUnitReporter({ classnameTemplate: `filename:{filename} - filepath:{filepath}` })
107+
const context = getContext()
108+
109+
// Act
110+
await reporter.onInit(context.vitest)
111+
112+
await reporter.onFinished(passedFiles)
113+
114+
// Assert
115+
expect(context.output).toMatchSnapshot()
116+
})
117+
63118
test('JUnit reporter (no outputFile entry)', async () => {
64119
// Arrange
65120
const reporter = new JUnitReporter({})

0 commit comments

Comments
 (0)