Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(reporter): add support for function type to classname option in the junit reporter #6839

Merged
merged 8 commits into from
Nov 18, 2024
4 changes: 2 additions & 2 deletions docs/guide/reporters.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,13 @@ AssertionError: expected 5 to be 4 // Object.is equality
</testsuites>
```

The outputted XML contains nested `testsuites` and `testcase` tags. You can use the environment variables `VITEST_JUNIT_SUITE_NAME` and `VITEST_JUNIT_CLASSNAME` to configure their `name` and `classname` attributes, respectively. These can also be customized via reporter options:
The outputted XML contains nested `testsuites` and `testcase` tags. You can use the environment variables `VITEST_JUNIT_SUITE_NAME` and `VITEST_JUNIT_CLASSNAME` to configure their `name` and `classname` attributes, respectively. These can also be customized via reporter options `suiteName` and `classnameTemplate`. `classnameTemplate` can either be a template string or a function.
jpleclerc marked this conversation as resolved.
Show resolved Hide resolved
hi-ogawa marked this conversation as resolved.
Show resolved Hide resolved

```ts
export default defineConfig({
test: {
reporters: [
['junit', { suiteName: 'custom suite name', classname: 'custom-classname' }]
['junit', { suiteName: 'custom suite name', classnameTemplate: 'filename:{filename} - filepath:{filepath} - suite:{suitename}' }]
]
},
})
Expand Down
35 changes: 34 additions & 1 deletion packages/vitest/src/node/reporters/junit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,21 @@ import { getOutputFile } from '../../utils/config-helpers'
import { capturePrintError } from '../error'
import { IndentedLogger } from './renderers/indented-logger'

interface ClassnameTemplateVariables {
filename: string
filepath: string
suitename: string
}

export interface JUnitOptions {
outputFile?: string
/** @deprecated Use `classnameTempalte` instead. */
jpleclerc marked this conversation as resolved.
Show resolved Hide resolved
classname?: string
hi-ogawa marked this conversation as resolved.
Show resolved Hide resolved

/**
* Template for the classname attribute. Can be either a string or a function. The string can contain placeholders like {filename}, {filepath}, {suitename}.
*/
classnameTemplate?: string | ((classnameVariables: ClassnameTemplateVariables) => string)
suiteName?: string
/**
* Write <system-out> and <system-err> for console output
Expand Down Expand Up @@ -195,10 +207,31 @@ export class JUnitReporter implements Reporter {

async writeTasks(tasks: Task[], filename: string): Promise<void> {
for (const task of tasks) {
let classname = filename

const templateVars: ClassnameTemplateVariables = {
filename: task.file.name,
filepath: task.file.filepath,
suitename: this.options.suiteName ?? task.suite?.name ?? '',
hi-ogawa marked this conversation as resolved.
Show resolved Hide resolved
}

if (typeof this.options.classnameTemplate === 'function') {
classname = this.options.classnameTemplate(templateVars)
}
else if (typeof this.options.classnameTemplate === 'string') {
classname = this.options.classnameTemplate
.replace(/\{filename\}/g, templateVars.filename)
.replace(/\{filepath\}/g, templateVars.filepath)
.replace(/\{suitename\}/g, templateVars.suitename)
}
else if (typeof this.options.classname === 'string') {
classname = this.options.classname
}

await this.writeElement(
'testcase',
{
classname: this.options.classname ?? filename,
classname,
file: this.options.addFileAttribute ? filename : undefined,
name: task.name,
time: getDuration(task),
Expand Down
34 changes: 33 additions & 1 deletion test/reporters/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,37 @@ const suite: Suite = {
tasks: [],
}

const passedFile: File = {
id: '1223128da3',
name: 'basic.test.ts',
type: 'suite',
suite,
meta: {},
mode: 'run',
filepath: '/vitest/test/core/test/basic.test.ts',
result: { state: 'pass', duration: 145.99284195899963 },
tasks: [
],
projectName: '',
file: null!,
}
passedFile.file = passedFile
passedFile.tasks.push({
id: '1223128da3_0_0',
type: 'test',
name: 'Math.sqrt()',
mode: 'run',
fails: undefined,
suite,
meta: {},
file: passedFile,
result: {
state: 'pass',
duration: 1.4422860145568848,
},
context: null as any,
})

const error: ErrorWithDiff = {
name: 'AssertionError',
message: 'expected 2.23606797749979 to equal 2',
Expand Down Expand Up @@ -176,5 +207,6 @@ file.tasks = [suite]
suite.tasks = tasks

const files = [file]
const passedFiles = [passedFile]

export { files }
export { files, passedFiles }
44 changes: 44 additions & 0 deletions test/reporters/tests/__snapshots__/reporters.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,39 @@ exports[`JUnit reporter 1`] = `
"
`;

exports[`JUnit reporter with custom function classnameTemplate 1`] = `
"<?xml version="1.0" encoding="UTF-8" ?>
<testsuites name="vitest tests" tests="1" failures="0" errors="0" time="0">
<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">
<testcase classname="filename:basic.test.ts - filepath:/vitest/test/core/test/basic.test.ts - suite:suite" name="Math.sqrt()" time="0.001442286">
</testcase>
</testsuite>
</testsuites>
"
`;

exports[`JUnit reporter with custom string classname 1`] = `
"<?xml version="1.0" encoding="UTF-8" ?>
<testsuites name="vitest tests" tests="1" failures="0" errors="0" time="0">
<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">
<testcase classname="my-custom-classname" name="Math.sqrt()" time="0.001442286">
</testcase>
</testsuite>
</testsuites>
"
`;

exports[`JUnit reporter with custom string classnameTemplate 1`] = `
"<?xml version="1.0" encoding="UTF-8" ?>
<testsuites name="vitest tests" tests="1" failures="0" errors="0" time="0">
<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">
<testcase classname="filename:basic.test.ts - filepath:/vitest/test/core/test/basic.test.ts - suite:suite" name="Math.sqrt()" time="0.001442286">
</testcase>
</testsuite>
</testsuites>
"
`;

exports[`JUnit reporter with outputFile 1`] = `
"JUNIT report written to <process-cwd>/report.xml
"
Expand Down Expand Up @@ -62,6 +95,17 @@ exports[`JUnit reporter with outputFile object in non-existing directory 2`] = `
"
`;

exports[`JUnit reporter without classname 1`] = `
"<?xml version="1.0" encoding="UTF-8" ?>
<testsuites name="vitest tests" tests="1" failures="0" errors="0" time="0">
<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">
<testcase classname="test/core/test/basic.test.ts" name="Math.sqrt()" time="0.001442286">
</testcase>
</testsuite>
</testsuites>
"
`;

exports[`json reporter (no outputFile entry) 1`] = `
{
"numFailedTestSuites": 1,
Expand Down
57 changes: 56 additions & 1 deletion test/reporters/tests/reporters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { JUnitReporter } from '../../../packages/vitest/src/node/reporters/junit
import { TapReporter } from '../../../packages/vitest/src/node/reporters/tap'
import { TapFlatReporter } from '../../../packages/vitest/src/node/reporters/tap-flat'
import { getContext } from '../src/context'
import { files } from '../src/data'
import { files, passedFiles } from '../src/data'

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

Expand Down Expand Up @@ -60,6 +60,61 @@ test('JUnit reporter', async () => {
expect(context.output).toMatchSnapshot()
})

test('JUnit reporter without classname', async () => {
// Arrange
const reporter = new JUnitReporter({})
const context = getContext()

// Act
await reporter.onInit(context.vitest)

await reporter.onFinished(passedFiles)

// Assert
expect(context.output).toMatchSnapshot()
})

test('JUnit reporter with custom string classname', async () => {
// Arrange
const reporter = new JUnitReporter({ classname: 'my-custom-classname' })
const context = getContext()

// Act
await reporter.onInit(context.vitest)

await reporter.onFinished(passedFiles)

// Assert
expect(context.output).toMatchSnapshot()
})

test('JUnit reporter with custom function classnameTemplate', async () => {
// Arrange
const reporter = new JUnitReporter({ classnameTemplate: task => `filename:${task.filename} - filepath:${task.filepath} - suite:${task.suitename}` })
const context = getContext()

// Act
await reporter.onInit(context.vitest)

await reporter.onFinished(passedFiles)

// Assert
expect(context.output).toMatchSnapshot()
})
test('JUnit reporter with custom string classnameTemplate', async () => {
// Arrange
const reporter = new JUnitReporter({ classnameTemplate: `filename:{filename} - filepath:{filepath} - suite:{suitename}` })
const context = getContext()

// Act
await reporter.onInit(context.vitest)

await reporter.onFinished(passedFiles)

// Assert
expect(context.output).toMatchSnapshot()
})

test('JUnit reporter (no outputFile entry)', async () => {
// Arrange
const reporter = new JUnitReporter({})
Expand Down