From cb67bf79a77e9acc9a0617efd717f3fb6cce1a0b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:09:40 +0900 Subject: [PATCH] feat: count repeated errors in failure header Co-Authored-By: Claude Opus 5.5 --- packages/vitest/src/node/reporters/base.ts | 35 ++++++++++++---------- test/e2e/test/retry.test.ts | 4 +-- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index 67305dc4a..615bc1960 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -1501,12 +1501,12 @@ export abstract class BaseReporter implements Reporter { for (const { error, sources } of errorsQueue) { const tasks = sources.map(({ task }) => task) - const attemptsByTask = new Map>() + const attemptsByTask = new Map() for (const { task, attempt } of sources) { - attemptsByTask.set(task, (attemptsByTask.get(task) ?? new Set()).add(attempt)) + attemptsByTask.set(task, [...(attemptsByTask.get(task) ?? []), attempt]) } const screenshotPaths = [...attemptsByTask].flatMap(([task, attempts]) => - [...attempts].flatMap((attempt) => + [...new Set(attempts)].flatMap((attempt) => attempt ? getFailureScreenshotPaths(task, attempt) : [], ), ) @@ -1559,20 +1559,25 @@ export abstract class BaseReporter implements Reporter { } } -function formatErrorAttempts( - task: Task, - attempts: Set, -): string | undefined { +function formatErrorAttempts(task: Task, attempts: (TaskResultAttempt | undefined)[]): string { const allAttempts = task.result?.attempts ?? [] - if (allAttempts.length <= 1) { - return - } const hasRepeats = allAttempts.some((attempt) => attempt.repeatIndex > 0) - return [...attempts] - .filter((attempt) => attempt !== undefined) - .map(({ repeatIndex, retryIndex }) => - hasRepeats ? `repeat ${repeatIndex} retry ${retryIndex}` : `retry ${retryIndex}`, - ) + return [...Map.groupBy(attempts, (attempt) => attempt)] + .map(([attempt, occurrences]) => { + const parts: string[] = [] + if (attempt && allAttempts.length > 1) { + parts.push( + hasRepeats + ? `repeat ${attempt.repeatIndex} retry ${attempt.retryIndex}` + : `retry ${attempt.retryIndex}`, + ) + } + if (occurrences.length > 1) { + parts.push(`x${occurrences.length}`) + } + return parts.join(' ') + }) + .filter(Boolean) .join(', ') } diff --git a/test/e2e/test/retry.test.ts b/test/e2e/test/retry.test.ts index 19c50af11..53ba92bfa 100644 --- a/test/e2e/test/retry.test.ts +++ b/test/e2e/test/retry.test.ts @@ -415,11 +415,11 @@ describe('failure summary header', () => { }) expect(extractFailureHeaders(stderr)).toMatchInlineSnapshot(` - " FAIL basic.test.js > soft-no-retry + " FAIL basic.test.js > soft-no-retry (x2) AssertionError: expected 1 to be 2 // Object.is equality FAIL basic.test.js > soft-no-retry AssertionError: expected 2 to be 3 // Object.is equality - FAIL basic.test.js > soft-retry (retry 0, retry 1) + FAIL basic.test.js > soft-retry (retry 0 x2, retry 1 x2) AssertionError: expected 1 to be 2 // Object.is equality FAIL basic.test.js > soft-retry (retry 0, retry 1) AssertionError: expected 2 to be 3 // Object.is equality" -- 2.54.0