From e774ead5e677a4dca8dcbb4401448327974e9143 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Leclerc Date: Fri, 1 Nov 2024 18:41:32 -0400 Subject: [PATCH 1/7] feat(reporter): added support function type support to classname option in the junit reporter --- packages/vitest/src/node/reporters/junit.ts | 6 ++- test/reporters/src/data.ts | 32 +++++++++++++- .../__snapshots__/reporters.spec.ts.snap | 33 ++++++++++++++ test/reporters/tests/reporters.spec.ts | 44 ++++++++++++++++++- 4 files changed, 111 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index c5e23b54174a..714d02fcd81d 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/packages/vitest/src/node/reporters/junit.ts @@ -13,7 +13,7 @@ import { IndentedLogger } from './renderers/indented-logger' export interface JUnitOptions { outputFile?: string - classname?: string + classname?: string | ((task: Task) => string) suiteName?: string /** * Write and for console output @@ -198,7 +198,9 @@ export class JUnitReporter implements Reporter { await this.writeElement( 'testcase', { - classname: this.options.classname ?? filename, + classname: typeof this.options.classname === 'function' + ? this.options.classname(task) + : this.options.classname ?? filename, file: this.options.addFileAttribute ? filename : undefined, name: task.name, time: getDuration(task), diff --git a/test/reporters/src/data.ts b/test/reporters/src/data.ts index c2b5b61fe788..255f8711f3a8 100644 --- a/test/reporters/src/data.ts +++ b/test/reporters/src/data.ts @@ -14,6 +14,35 @@ const file: File = { } file.file = file +const passedFile: File = { + id: '1223128da3', + name: 'test/core/test/basic.test.ts', + type: 'suite', + meta: {}, + mode: 'run', + filepath: '/vitest/test/core/test/basic.test.ts', + result: { state: 'pass', duration: 145.99284195899963 }, + tasks: [ + { + id: '1223128da3_0_0', + type: 'test', + name: 'Math.sqrt()', + mode: 'run', + fails: undefined, + meta: {}, + file, + result: { + state: 'pass', + duration: 1.4422860145568848, + }, + context: null as any, + }, + ], + projectName: '', + file: null!, +} +passedFile.file = passedFile + const suite: Suite = { id: '1223128da3_0', type: 'suite', @@ -176,5 +205,6 @@ file.tasks = [suite] suite.tasks = tasks const files = [file] +const passedFiles = [passedFile] -export { files } +export { files, passedFiles } diff --git a/test/reporters/tests/__snapshots__/reporters.spec.ts.snap b/test/reporters/tests/__snapshots__/reporters.spec.ts.snap index 32d3fc3f7ab1..6b2dc03d5cd9 100644 --- a/test/reporters/tests/__snapshots__/reporters.spec.ts.snap +++ b/test/reporters/tests/__snapshots__/reporters.spec.ts.snap @@ -14,6 +14,28 @@ exports[`JUnit reporter 1`] = ` " `; +exports[`JUnit reporter with custom function classname 1`] = ` +" + + + + + + +" +`; + +exports[`JUnit reporter with custom string classname 1`] = ` +" + + + + + + +" +`; + exports[`JUnit reporter with outputFile 1`] = ` "JUNIT report written to /report.xml " @@ -62,6 +84,17 @@ exports[`JUnit reporter with outputFile object in non-existing directory 2`] = ` " `; +exports[`JUnit reporter without classname 1`] = ` +" + + + + + + +" +`; + exports[`json reporter (no outputFile entry) 1`] = ` { "numFailedTestSuites": 1, diff --git a/test/reporters/tests/reporters.spec.ts b/test/reporters/tests/reporters.spec.ts index 0b6a0d518ad8..c55e1ff592c0 100644 --- a/test/reporters/tests/reporters.spec.ts +++ b/test/reporters/tests/reporters.spec.ts @@ -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) @@ -60,6 +60,48 @@ 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 classname', async () => { + // Arrange + const reporter = new JUnitReporter({ classname: task => `file:${task.file.name}` }) + 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({}) From b0617065628fc9e758068ef11b49f63e91702911 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Leclerc Date: Mon, 4 Nov 2024 11:42:36 -0500 Subject: [PATCH 2/7] feat(reporter): created a new option for the junit reporter. - classnameTemplate can take a function or a string - the string can be parameterized Change-Id: I6a79ed8f89e23e518c8d18f6fe49a210edaa247a --- packages/vitest/src/node/reporters/junit.ts | 36 +++++++++++-- test/reporters/src/data.ts | 50 ++++++++++--------- .../__snapshots__/reporters.spec.ts.snap | 15 +++++- test/reporters/tests/reporters.spec.ts | 17 ++++++- 4 files changed, 86 insertions(+), 32 deletions(-) diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index 714d02fcd81d..8490ef41d751 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/packages/vitest/src/node/reporters/junit.ts @@ -11,9 +11,20 @@ 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 - classname?: string | ((task: Task) => string) + classname?: string + + /** + * 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 and for console output @@ -195,12 +206,29 @@ export class JUnitReporter implements Reporter { async writeTasks(tasks: Task[], filename: string): Promise { for (const task of tasks) { + let classname = filename + + if (typeof this.options.classnameTemplate === 'function') { + classname = this.options.classnameTemplate({ + filename: task.file.name, + filepath: task.file.filepath, + suitename: task.suite?.name ?? '', + }) + } + else if (typeof this.options.classnameTemplate === 'string') { + classname = this.options.classnameTemplate + .replace(/\{filename\}/g, task.file.name) + .replace(/\{filepath\}/g, task.file.filepath) + .replace(/\{suitename\}/g, task.suite?.name ?? '') + } + else if (typeof this.options.classname === 'string') { + classname = this.options.classname + } + await this.writeElement( 'testcase', { - classname: typeof this.options.classname === 'function' - ? this.options.classname(task) - : this.options.classname ?? filename, + classname, file: this.options.addFileAttribute ? filename : undefined, name: task.name, time: getDuration(task), diff --git a/test/reporters/src/data.ts b/test/reporters/src/data.ts index 255f8711f3a8..786341eabb7e 100644 --- a/test/reporters/src/data.ts +++ b/test/reporters/src/data.ts @@ -14,45 +14,47 @@ const file: File = { } file.file = file +const suite: Suite = { + id: '1223128da3_0', + type: 'suite', + name: 'suite', + mode: 'run', + meta: {}, + file, + result: { state: 'pass', duration: 1.90183687210083 }, + tasks: [], +} + const passedFile: File = { id: '1223128da3', - name: 'test/core/test/basic.test.ts', + 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: [ - { - id: '1223128da3_0_0', - type: 'test', - name: 'Math.sqrt()', - mode: 'run', - fails: undefined, - meta: {}, - file, - result: { - state: 'pass', - duration: 1.4422860145568848, - }, - context: null as any, - }, ], projectName: '', file: null!, } passedFile.file = passedFile - -const suite: Suite = { - id: '1223128da3_0', - type: 'suite', - name: 'suite', +passedFile.tasks.push({ + id: '1223128da3_0_0', + type: 'test', + name: 'Math.sqrt()', mode: 'run', + fails: undefined, + suite, meta: {}, - file, - result: { state: 'pass', duration: 1.90183687210083 }, - tasks: [], -} + file: passedFile, + result: { + state: 'pass', + duration: 1.4422860145568848, + }, + context: null as any, +}) const error: ErrorWithDiff = { name: 'AssertionError', diff --git a/test/reporters/tests/__snapshots__/reporters.spec.ts.snap b/test/reporters/tests/__snapshots__/reporters.spec.ts.snap index 6b2dc03d5cd9..eff3379b8fa9 100644 --- a/test/reporters/tests/__snapshots__/reporters.spec.ts.snap +++ b/test/reporters/tests/__snapshots__/reporters.spec.ts.snap @@ -14,11 +14,11 @@ exports[`JUnit reporter 1`] = ` " `; -exports[`JUnit reporter with custom function classname 1`] = ` +exports[`JUnit reporter with custom function classnameTemplate 1`] = ` " - + @@ -36,6 +36,17 @@ exports[`JUnit reporter with custom string classname 1`] = ` " `; +exports[`JUnit reporter with custom string classnameTemplate 1`] = ` +" + + + + + + +" +`; + exports[`JUnit reporter with outputFile 1`] = ` "JUNIT report written to /report.xml " diff --git a/test/reporters/tests/reporters.spec.ts b/test/reporters/tests/reporters.spec.ts index c55e1ff592c0..0f8fa4d75ec5 100644 --- a/test/reporters/tests/reporters.spec.ts +++ b/test/reporters/tests/reporters.spec.ts @@ -88,9 +88,22 @@ test('JUnit reporter with custom string classname', async () => { expect(context.output).toMatchSnapshot() }) -test('JUnit reporter with custom function classname', async () => { +test('JUnit reporter with custom function classnameTemplate', async () => { // Arrange - const reporter = new JUnitReporter({ classname: task => `file:${task.file.name}` }) + 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 From 9bad3c8e0b2c54aa7fe7b318d55939690d9f3231 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Leclerc Date: Mon, 4 Nov 2024 11:51:32 -0500 Subject: [PATCH 3/7] feat(reporter): default to options.suiteName for the suitename in the classnameTemplate Change-Id: I03a3d47d26aa36ca5c14cf790a19629d151b7c61 --- packages/vitest/src/node/reporters/junit.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index 8490ef41d751..19c6f94adc01 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/packages/vitest/src/node/reporters/junit.ts @@ -208,18 +208,20 @@ export class JUnitReporter implements Reporter { 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 ?? '', + } + if (typeof this.options.classnameTemplate === 'function') { - classname = this.options.classnameTemplate({ - filename: task.file.name, - filepath: task.file.filepath, - suitename: task.suite?.name ?? '', - }) + classname = this.options.classnameTemplate(templateVars) } else if (typeof this.options.classnameTemplate === 'string') { classname = this.options.classnameTemplate - .replace(/\{filename\}/g, task.file.name) - .replace(/\{filepath\}/g, task.file.filepath) - .replace(/\{suitename\}/g, task.suite?.name ?? '') + .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 From 23448c0c7e47aa6fa6db6541b0fabe68bf8bbbf8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 5 Nov 2024 08:32:19 +0900 Subject: [PATCH 4/7] chore: deprecate `classname` --- packages/vitest/src/node/reporters/junit.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index 19c6f94adc01..949eeba3eae0 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/packages/vitest/src/node/reporters/junit.ts @@ -19,6 +19,7 @@ interface ClassnameTemplateVariables { export interface JUnitOptions { outputFile?: string + /** @deprecated Use `classnameTempalte` instead. */ classname?: string /** From 04add0c0fa9a7418497a6d442b06b2522494ae5d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Leclerc Date: Thu, 7 Nov 2024 12:37:58 -0500 Subject: [PATCH 5/7] feat(reporter): Mention classnameTemplate in the doc Change-Id: I1caa6e359998071ced9839ad27d121d25e8b8d99 --- docs/guide/reporters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/reporters.md b/docs/guide/reporters.md index 3f97088ecead..f58ab257391a 100644 --- a/docs/guide/reporters.md +++ b/docs/guide/reporters.md @@ -249,13 +249,13 @@ AssertionError: expected 5 to be 4 // Object.is equality ``` -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. ```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}' }] ] }, }) From 2e3dc97e3b483b6f8d127545e4688d5d50d7feb2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Leclerc Date: Mon, 11 Nov 2024 17:01:06 -0500 Subject: [PATCH 6/7] feat(reporter): Sightly improved documentation for junit reporter and fixed typo. Change-Id: Ie77e0955011fa42d2a8ef42c074327589d780739 --- docs/guide/reporters.md | 5 +++++ packages/vitest/src/node/reporters/junit.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/guide/reporters.md b/docs/guide/reporters.md index f58ab257391a..9ebfc1b21aa8 100644 --- a/docs/guide/reporters.md +++ b/docs/guide/reporters.md @@ -251,6 +251,11 @@ AssertionError: expected 5 to be 4 // Object.is equality 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. +The supported placeholders for the `classnameTemplate` option are: +- filename +- filepath +- suitename + ```ts export default defineConfig({ test: { diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index 949eeba3eae0..e66961573a07 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/packages/vitest/src/node/reporters/junit.ts @@ -19,7 +19,7 @@ interface ClassnameTemplateVariables { export interface JUnitOptions { outputFile?: string - /** @deprecated Use `classnameTempalte` instead. */ + /** @deprecated Use `classnameTemplate` instead. */ classname?: string /** From fd41ff28178a2e275a2692ec9dcb41df6a1ef4bf Mon Sep 17 00:00:00 2001 From: Jean-Philippe Leclerc Date: Thu, 14 Nov 2024 10:40:24 -0500 Subject: [PATCH 7/7] feat(reporter): Removed the mention of suitename in classnameTemplate Change-Id: I6d4e936a6cabaacf31a82036acf22f9af401bdb0 --- docs/guide/reporters.md | 3 +-- packages/vitest/src/node/reporters/junit.ts | 5 +---- test/reporters/tests/__snapshots__/reporters.spec.ts.snap | 4 ++-- test/reporters/tests/reporters.spec.ts | 4 ++-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/guide/reporters.md b/docs/guide/reporters.md index 9ebfc1b21aa8..67e17e0c4e23 100644 --- a/docs/guide/reporters.md +++ b/docs/guide/reporters.md @@ -254,13 +254,12 @@ The outputted XML contains nested `testsuites` and `testcase` tags. You can use The supported placeholders for the `classnameTemplate` option are: - filename - filepath -- suitename ```ts export default defineConfig({ test: { reporters: [ - ['junit', { suiteName: 'custom suite name', classnameTemplate: 'filename:{filename} - filepath:{filepath} - suite:{suitename}' }] + ['junit', { suiteName: 'custom suite name', classnameTemplate: 'filename:{filename} - filepath:{filepath}' }] ] }, }) diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index e66961573a07..d6185813d133 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/packages/vitest/src/node/reporters/junit.ts @@ -14,7 +14,6 @@ import { IndentedLogger } from './renderers/indented-logger' interface ClassnameTemplateVariables { filename: string filepath: string - suitename: string } export interface JUnitOptions { @@ -23,7 +22,7 @@ export interface JUnitOptions { classname?: string /** - * Template for the classname attribute. Can be either a string or a function. The string can contain placeholders like {filename}, {filepath}, {suitename}. + * Template for the classname attribute. Can be either a string or a function. The string can contain placeholders {filename} and {filepath}. */ classnameTemplate?: string | ((classnameVariables: ClassnameTemplateVariables) => string) suiteName?: string @@ -212,7 +211,6 @@ export class JUnitReporter implements Reporter { const templateVars: ClassnameTemplateVariables = { filename: task.file.name, filepath: task.file.filepath, - suitename: this.options.suiteName ?? task.suite?.name ?? '', } if (typeof this.options.classnameTemplate === 'function') { @@ -222,7 +220,6 @@ export class JUnitReporter implements Reporter { 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 diff --git a/test/reporters/tests/__snapshots__/reporters.spec.ts.snap b/test/reporters/tests/__snapshots__/reporters.spec.ts.snap index eff3379b8fa9..bf21bff29f27 100644 --- a/test/reporters/tests/__snapshots__/reporters.spec.ts.snap +++ b/test/reporters/tests/__snapshots__/reporters.spec.ts.snap @@ -18,7 +18,7 @@ exports[`JUnit reporter with custom function classnameTemplate 1`] = ` " - + @@ -40,7 +40,7 @@ exports[`JUnit reporter with custom string classnameTemplate 1`] = ` " - + diff --git a/test/reporters/tests/reporters.spec.ts b/test/reporters/tests/reporters.spec.ts index 0f8fa4d75ec5..b52ed51ce6d3 100644 --- a/test/reporters/tests/reporters.spec.ts +++ b/test/reporters/tests/reporters.spec.ts @@ -90,7 +90,7 @@ test('JUnit reporter with custom string classname', async () => { 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 reporter = new JUnitReporter({ classnameTemplate: task => `filename:${task.filename} - filepath:${task.filepath}` }) const context = getContext() // Act @@ -103,7 +103,7 @@ test('JUnit reporter with custom function classnameTemplate', async () => { }) test('JUnit reporter with custom string classnameTemplate', async () => { // Arrange - const reporter = new JUnitReporter({ classnameTemplate: `filename:{filename} - filepath:{filepath} - suite:{suitename}` }) + const reporter = new JUnitReporter({ classnameTemplate: `filename:{filename} - filepath:{filepath}` }) const context = getContext() // Act