1+ import { existsSync } from "node:fs" ;
12import { createRequire } from "node:module" ;
3+ import { basename , dirname , join } from "node:path" ;
24import { fileURLToPath } from "node:url" ;
35
4- import { Stage , Status } from "allure-js-commons" ;
6+ import { getTestName } from "@vitest/runner/utils" ;
7+ import { ContentType , Stage , Status } from "allure-js-commons" ;
58import type { RuntimeMessage } from "allure-js-commons/sdk" ;
69import { getMessageAndTraceFromError , getStatusFromError , toGlobalErrorMessage } from "allure-js-commons/sdk" ;
710import type { ReporterConfig } from "allure-js-commons/sdk/reporter" ;
@@ -44,6 +47,29 @@ const browserSetupModulePath = fileURLToPath(new URL("./browser/setup.js", impor
4447const normalizeSetupFilePath = ( setupFilePath : string ) =>
4548 setupFilePath . startsWith ( "file://" ) ? fileURLToPath ( setupFilePath ) : setupFilePath ;
4649
50+ type TaskResultLike = NonNullable < Task [ "result" ] > ;
51+
52+ type VitestArtifactAttachment = {
53+ name ?: string ;
54+ path ?: string ;
55+ body ?: string | Uint8Array ;
56+ contentType ?: string ;
57+ } ;
58+
59+ type VitestArtifact = {
60+ type ?: string ;
61+ message ?: string ;
62+ attachments ?: VitestArtifactAttachment [ ] ;
63+ } ;
64+
65+ type AllureVitestAttempt = {
66+ result ?: TaskResultLike ;
67+ runtimeMessages ?: RuntimeMessage [ ] ;
68+ artifacts ?: VitestArtifact [ ] ;
69+ } ;
70+
71+ const sanitizeTracePart = ( value : string ) : string => value . replaceAll ( / [ ^ a - z 0 - 9 ] / gi, "-" ) ;
72+
4773export default class AllureVitestReporter implements Reporter {
4874 private allureReporterRuntime ?: ReporterRuntime ;
4975 private config : AllureVitestReporterConfig ;
@@ -144,8 +170,7 @@ export default class AllureVitestReporter implements Reporter {
144170 const {
145171 allureRuntimeMessages = [ ] ,
146172 allureGlobalRuntimeMessages = [ ] ,
147- vitestWorker,
148- browser,
173+ allureFailedAttempts = [ ] ,
149174 allureSkip = false ,
150175 } = task . meta ;
151176
@@ -158,6 +183,33 @@ export default class AllureVitestReporter implements Reporter {
158183 this . globalRuntimeMessages . push ( ...allureGlobalRuntimeMessages ) ;
159184 }
160185
186+ const finalAttemptResult = task . result
187+ ? {
188+ ...task . result ,
189+ startTime : task . meta . allureAttemptStartTime ?? task . result . startTime ,
190+ duration : task . meta . allureAttemptDuration ?? task . result . duration ,
191+ }
192+ : undefined ;
193+
194+ const attempts : AllureVitestAttempt [ ] = [
195+ ...( allureFailedAttempts as AllureVitestAttempt [ ] ) ,
196+ ...( task . result ?. state === "fail" && allureFailedAttempts . length ? [ ] : [ { } ] ) ,
197+ ] ;
198+
199+ for ( const attempt of attempts ) {
200+ this . writeTaskAttempt ( task , {
201+ result : attempt . result ?? finalAttemptResult ,
202+ runtimeMessages : attempt . runtimeMessages ?? allureRuntimeMessages ,
203+ artifacts : attempt . artifacts ?? ( task . artifacts as VitestArtifact [ ] | undefined ) ,
204+ } ) ;
205+ }
206+ }
207+
208+ private writeTaskAttempt (
209+ task : Task ,
210+ { result : taskResult , runtimeMessages : allureRuntimeMessages = [ ] , artifacts = [ ] } : AllureVitestAttempt ,
211+ ) {
212+ const { vitestWorker, browser } = task . meta ;
161213 const {
162214 projectName,
163215 specPath,
@@ -170,7 +222,7 @@ export default class AllureVitestReporter implements Reporter {
170222 } = getTestMetadata ( task ) ;
171223 const testUuid = this . allureReporterRuntime ! . startTest ( {
172224 name,
173- start : task . result ?. startTime ,
225+ start : taskResult ?. startTime ,
174226 } ) ;
175227
176228 this . allureReporterRuntime ! . updateTest ( testUuid , ( result ) => {
@@ -211,9 +263,9 @@ export default class AllureVitestReporter implements Reporter {
211263 this . allureReporterRuntime ! . applyRuntimeMessages ( testUuid , runtimeMessages ) ;
212264 }
213265
214- switch ( task . result ?. state ) {
266+ switch ( taskResult ?. state ) {
215267 case "fail" : {
216- const [ error ] = task . result . errors || [ ] ;
268+ const [ error ] = taskResult . errors || [ ] ;
217269 const status = getStatusFromError ( error ) ;
218270
219271 result . statusDetails = {
@@ -234,11 +286,71 @@ export default class AllureVitestReporter implements Reporter {
234286 break ;
235287 }
236288 }
289+
290+ if ( ( taskResult ?. retryCount ?? 0 ) > 0 ) {
291+ result . parameters . push ( { name : "Retry" , value : String ( taskResult ?. retryCount ) , excluded : true } ) ;
292+ }
237293 } ) ;
238294
239- this . allureReporterRuntime ! . stopTest ( testUuid , { duration : task . result ?. duration ?? 0 } ) ;
295+ this . writeTaskArtifacts ( testUuid , taskResult , artifacts ) ;
296+ this . writeTaskTrace ( testUuid , task , taskResult ) ;
297+
298+ this . allureReporterRuntime ! . stopTest ( testUuid , { duration : taskResult ?. duration ?? 0 } ) ;
240299 this . allureReporterRuntime ! . writeTest ( testUuid ) ;
241300 }
301+
302+ private writeTaskArtifacts ( testUuid : string , taskResult : TaskResultLike | undefined , artifacts : VitestArtifact [ ] ) {
303+ if ( taskResult ?. state !== "fail" ) {
304+ return ;
305+ }
306+
307+ for ( const artifact of artifacts ) {
308+ if ( artifact . type === "internal:failureScreenshot" ) {
309+ this . writeArtifactAttachments ( testUuid , artifact , "Failure screenshot" ) ;
310+ }
311+
312+ if ( artifact . type === "internal:toMatchScreenshot" ) {
313+ this . writeArtifactAttachments ( testUuid , artifact , artifact . message ?? "Screenshot diff" ) ;
314+ }
315+ }
316+ }
317+
318+ private writeArtifactAttachments ( testUuid : string , artifact : VitestArtifact , fallbackName : string ) {
319+ for ( const attachment of artifact . attachments ?? [ ] ) {
320+ const contentType = attachment . contentType ?? ContentType . PNG ;
321+ const name = attachment . name ?? ( attachment . path ? basename ( attachment . path ) : fallbackName ) ;
322+
323+ if ( attachment . path ) {
324+ this . allureReporterRuntime ! . writeAttachment ( testUuid , undefined , name , attachment . path , {
325+ contentType,
326+ } ) ;
327+ continue ;
328+ }
329+
330+ if ( attachment . body ) {
331+ this . allureReporterRuntime ! . writeAttachment ( testUuid , undefined , name , Buffer . from ( attachment . body ) , {
332+ contentType,
333+ } ) ;
334+ }
335+ }
336+ }
337+
338+ private writeTaskTrace ( testUuid : string , task : Task , taskResult : TaskResultLike | undefined ) {
339+ if ( taskResult ?. state !== "fail" ) {
340+ return ;
341+ }
342+
343+ const tracePath = getBrowserTracePath ( task , taskResult ) ;
344+
345+ if ( ! tracePath || ! existsSync ( tracePath ) ) {
346+ return ;
347+ }
348+
349+ this . allureReporterRuntime ! . writeAttachment ( testUuid , undefined , "Trace" , tracePath , {
350+ contentType : ContentType . PLAYWRIGHT_TRACE ,
351+ fileExtension : ".zip" ,
352+ } ) ;
353+ }
242354}
243355
244356const getHookGlobalErrorMessages = ( task : Task ) : RuntimeMessage [ ] => {
@@ -276,3 +388,30 @@ const getHookGlobalErrorMessages = (task: Task): RuntimeMessage[] => {
276388 ] ;
277389 } ) ;
278390} ;
391+
392+ const getBrowserTracePath = ( task : Task , result : TaskResultLike ) : string | undefined => {
393+ if ( ! task . meta . browser || ! task . file ?. filepath ) {
394+ return undefined ;
395+ }
396+
397+ const testFilePath = task . file . filepath ;
398+ const projectName = task . file . projectName ?? task . meta . browser ;
399+ const repeatCount = result . repeatCount ?? 0 ;
400+ const retryCount = result . retryCount ?? 0 ;
401+ const traceDirectory = join ( dirname ( testFilePath ) , "__traces__" , basename ( testFilePath ) ) ;
402+ // Vitest browser trace names are deterministic but internal; prefer its own full test name formatting.
403+ const traceFileNames = [
404+ [ sanitizeTracePart ( projectName ) , sanitizeTracePart ( getTestName ( task , "-" ) ) , repeatCount , retryCount ] . join ( "-" ) ,
405+ [ sanitizeTracePart ( projectName ) , sanitizeTracePart ( getTestMetadata ( task ) . name ) , repeatCount , retryCount ] . join ( "-" ) ,
406+ ] ;
407+
408+ for ( const traceFileName of traceFileNames ) {
409+ const tracePath = join ( traceDirectory , `${ traceFileName } .trace.zip` ) ;
410+
411+ if ( existsSync ( tracePath ) ) {
412+ return tracePath ;
413+ }
414+ }
415+
416+ return undefined ;
417+ } ;
0 commit comments