···11+import type { ProvidedContext } from '../types/general'
22+import type { ResolvedConfig, ResolvedProjectConfig, SerializedConfig } from './types/config'
33+import type { WorkspaceProject } from './workspace'
44+import type { Vitest } from './core'
55+66+export class TestProject {
77+ /**
88+ * The global vitest instance.
99+ * @experimental The public Vitest API is experimental and does not follow semver.
1010+ */
1111+ public readonly vitest: Vitest
1212+ /**
1313+ * The workspace project this test project is associated with.
1414+ * @experimental The public Vitest API is experimental and does not follow semver.
1515+ */
1616+ public readonly workspaceProject: WorkspaceProject
1717+1818+ /**
1919+ * Resolved project configuration.
2020+ */
2121+ public readonly config: ResolvedProjectConfig
2222+ /**
2323+ * Resolved global configuration. If there are no workspace projects, this will be the same as `config`.
2424+ */
2525+ public readonly globalConfig: ResolvedConfig
2626+2727+ /**
2828+ * The name of the project or an empty string if not set.
2929+ */
3030+ public readonly name: string
3131+3232+ constructor(workspaceProject: WorkspaceProject) {
3333+ this.workspaceProject = workspaceProject
3434+ this.vitest = workspaceProject.ctx
3535+ this.globalConfig = workspaceProject.ctx.config
3636+ this.config = workspaceProject.config
3737+ this.name = workspaceProject.getName()
3838+ }
3939+4040+ /**
4141+ * Serialized project configuration. This is the config that tests receive.
4242+ */
4343+ public get serializedConfig() {
4444+ return this.workspaceProject.getSerializableConfig()
4545+ }
4646+4747+ /**
4848+ * Custom context provided to the project.
4949+ */
5050+ public context(): ProvidedContext {
5151+ return this.workspaceProject.getProvidedContext()
5252+ }
5353+5454+ /**
5555+ * Provide a custom context to the project. This context will be available for tests once they run.
5656+ */
5757+ public provide<T extends keyof ProvidedContext & string>(
5858+ key: T,
5959+ value: ProvidedContext[T],
6060+ ): void {
6161+ this.workspaceProject.provide(key, value)
6262+ }
6363+6464+ public toJSON(): SerializedTestProject {
6565+ return {
6666+ name: this.name,
6767+ serializedConfig: this.serializedConfig,
6868+ context: this.context(),
6969+ }
7070+ }
7171+}
7272+7373+interface SerializedTestProject {
7474+ name: string
7575+ serializedConfig: SerializedConfig
7676+ context: ProvidedContext
7777+}
+28-12
packages/vitest/src/node/state.ts
···11import type { File, Task, TaskResultPack } from '@vitest/runner'
22-33-// can't import actual functions from utils, because it's incompatible with @vitest/browsers
42import { createFileTask } from '@vitest/runner/utils'
53import type { AggregateError as AggregateErrorPonyfill } from '../utils/base'
64import type { UserConsoleLog } from '../types/general'
75import type { WorkspaceProject } from './workspace'
66+import { TestCase, TestFile, TestSuite } from './reporters/reported-tasks'
8798export function isAggregateError(err: unknown): err is AggregateErrorPonyfill {
109 if (typeof AggregateError !== 'undefined' && err instanceof AggregateError) {
···1413 return err instanceof Error && 'errors' in err
1514}
16151717-// Note this file is shared for both node and browser, be aware to avoid node specific logic
1816export class StateManager {
1917 filesMap = new Map<string, File[]>()
2018 pathsSet: Set<string> = new Set()
···2220 taskFileMap = new WeakMap<Task, File>()
2321 errorsSet = new Set<unknown>()
2422 processTimeoutCauses = new Set<string>()
2323+ reportedTasksMap = new WeakMap<Task, TestCase | TestFile | TestSuite>()
25242625 catchError(err: unknown, type: string): void {
2726 if (isAggregateError(err)) {
···9897 })
9998 }
10099101101- collectFiles(files: File[] = []) {
100100+ collectFiles(project: WorkspaceProject, files: File[] = []) {
102101 files.forEach((file) => {
103102 const existing = this.filesMap.get(file.filepath) || []
104103 const otherProject = existing.filter(
···114113 }
115114 otherProject.push(file)
116115 this.filesMap.set(file.filepath, otherProject)
117117- this.updateId(file)
116116+ this.updateId(file, project)
118117 })
119118 }
120119121121- // this file is reused by ws-client, and should not rely on heavy dependencies like workspace
122120 clearFiles(
123123- _project: { config: { name: string | undefined; root: string } },
121121+ project: WorkspaceProject,
124122 paths: string[] = [],
125123 ) {
126126- const project = _project as WorkspaceProject
127124 paths.forEach((path) => {
128125 const files = this.filesMap.get(path)
129126 const fileTask = createFileTask(
···132129 project.config.name,
133130 )
134131 fileTask.local = true
132132+ TestFile.register(fileTask, project)
135133 this.idMap.set(fileTask.id, fileTask)
136134 if (!files) {
137135 this.filesMap.set(path, [fileTask])
···150148 })
151149 }
152150153153- updateId(task: Task) {
151151+ updateId(task: Task, project: WorkspaceProject) {
154152 if (this.idMap.get(task.id) === task) {
155153 return
156154 }
155155+156156+ if (task.type === 'suite' && 'filepath' in task) {
157157+ TestFile.register(task, project)
158158+ }
159159+ else if (task.type === 'suite') {
160160+ TestSuite.register(task, project)
161161+ }
162162+ else {
163163+ TestCase.register(task, project)
164164+ }
165165+157166 this.idMap.set(task.id, task)
158167 if (task.type === 'suite') {
159168 task.tasks.forEach((task) => {
160160- this.updateId(task)
169169+ this.updateId(task, project)
161170 })
162171 }
172172+ }
173173+174174+ getReportedEntity(task: Task) {
175175+ return this.reportedTasksMap.get(task)
163176 }
164177165178 updateTasks(packs: TaskResultPack[]) {
···192205 ).length
193206 }
194207195195- cancelFiles(files: string[], root: string, projectName: string) {
208208+ cancelFiles(files: string[], project: WorkspaceProject) {
196209 this.collectFiles(
197197- files.map(filepath => createFileTask(filepath, root, projectName)),
210210+ project,
211211+ files.map(filepath =>
212212+ createFileTask(filepath, project.config.root, project.config.name),
213213+ ),
198214 )
199215 }
200216}
+20-24
packages/vitest/src/node/workspace.ts
···11import { promises as fs } from 'node:fs'
22-import { rm } from 'node:fs/promises'
32import { tmpdir } from 'node:os'
33+import { rm } from 'node:fs/promises'
44import fg from 'fast-glob'
55import mm from 'micromatch'
66import {
···2424import type { ProvidedContext } from '../types/general'
2525import type {
2626 ResolvedConfig,
2727+ SerializedConfig,
2728 UserConfig,
2829 UserWorkspaceConfig,
2930} from './types/config'
···3738import { CoverageTransform } from './plugins/coverageTransform'
3839import { serializeConfig } from './config/serializeConfig'
3940import type { Vitest } from './core'
4141+import { TestProject } from './reported-workspace-project'
40424143interface InitializeProjectOptions extends UserWorkspaceConfig {
4244 workspaceConfigPath: string
···9597 closingPromise: Promise<unknown> | undefined
96989799 testFilesList: string[] | null = null
100100+101101+ public testProject!: TestProject
9810299103 public readonly id = nanoid()
100104 public readonly tmpDir = join(tmpdir(), this.id)
···204208 getSourceMapModuleById(id: string): TransformResult['map'] | undefined {
205209 const mod = this.server.moduleGraph.getModuleById(id)
206210 return mod?.ssrTransformResult?.map || mod?.transformResult?.map
207207- }
208208-209209- getBrowserSourceMapModuleById(
210210- id: string,
211211- ): TransformResult['map'] | undefined {
212212- return this.browser?.vite.moduleGraph.getModuleById(id)?.transformResult?.map
213211 }
214212215213 get reporters() {
···366364 project.server = ctx.server
367365 project.runner = ctx.runner
368366 project.config = ctx.config
367367+ project.testProject = new TestProject(project)
369368 return project
370369 }
371370···385384 server.config,
386385 this.ctx.logger,
387386 )
387387+ this.testProject = new TestProject(this)
388388389389 this.server = server
390390···404404 await this.initBrowserServer(this.server.config.configFile)
405405 }
406406407407- isBrowserEnabled() {
407407+ isBrowserEnabled(): boolean {
408408 return isBrowserEnabled(this.config)
409409 }
410410411411- getSerializableConfig(method: 'run' | 'collect' = 'run') {
412412- // TODO: call `serializeConfig` only once
413413- const config = deepMerge(serializeConfig(
411411+ getSerializableConfig(): SerializedConfig {
412412+ // TODO: serialize the config _once_ or when needed
413413+ const config = serializeConfig(
414414 this.config,
415415 this.ctx.config,
416416- this.server?.config,
417417- ), (this.ctx.configOverride || {}))
418418-419419- // disable heavy features when collecting because they are not needed
420420- if (method === 'collect') {
421421- if (this.config.browser.provider && this.config.browser.provider !== 'preview') {
422422- config.browser.headless = true
423423- }
424424- config.snapshotSerializers = []
425425- config.diff = undefined
416416+ this.server.config,
417417+ )
418418+ if (!this.ctx.configOverride) {
419419+ return config
426420 }
427427-428428- return config
421421+ return deepMerge(
422422+ config,
423423+ this.ctx.configOverride,
424424+ )
429425 }
430426431427 close() {
···444440445441 private async clearTmpDir() {
446442 try {
447447- await rm(this.tmpDir, { force: true, recursive: true })
443443+ await rm(this.tmpDir, { recursive: true })
448444 }
449445 catch {}
450446 }
+25-5
packages/vitest/src/public/index.ts
···33import '../types/global'
4455import type {
66+ Custom as Custom_,
77+ File as File_,
88+ Suite as Suite_,
99+ Task as Task_,
1010+ Test as Test_,
1111+} from '@vitest/runner'
1212+import type {
613 CollectLineNumbers as CollectLineNumbers_,
714 CollectLines as CollectLines_,
815 Context as Context_,
···120127/** @deprecated import `TypeCheckContext` from `vitest/node` instead */
121128export type Context = Context_
122129130130+/** @deprecated use `RunnerTestSuite` instead */
131131+export type Suite = Suite_
132132+/** @deprecated use `RunnerTestFile` instead */
133133+export type File = File_
134134+/** @deprecated use `RunnerTestCase` instead */
135135+export type Test = Test_
136136+/** @deprecated use `RunnerCustomCase` instead */
137137+export type Custom = Custom_
138138+/** @deprecated use `RunnerTask` instead */
139139+export type Task = Task_
140140+123141export type {
124142 RunMode,
125143 TaskState,
126144 TaskBase,
127145 TaskResult,
128146 TaskResultPack,
129129- Suite,
130130- File,
131131- Test,
132132- Task,
147147+ Suite as RunnerTestSuite,
148148+ File as RunnerTestFile,
149149+ Test as RunnerTestCase,
150150+ Task as RunnerTask,
151151+ Custom as RunnerCustomCase,
133152 DoneCallback,
134153 TestFunction,
135154 TestOptions,
···144163 TestContext,
145164 TaskContext,
146165 ExtendedContext,
147147- Custom,
148166 TaskCustomOptions,
149167 OnTestFailedHandler,
150168 TaskMeta,
···200218 ProvidedContext,
201219 AfterSuiteRunMeta,
202220} from '../types/general'
221221+222222+export type { TestError, SerializedError } from '@vitest/utils'
203223204224/** @deprecated import from `vitest/environments` instead */
205225export type EnvironmentReturn = EnvironmentReturn_
+15
packages/vitest/src/public/node.ts
···4848export { isFileServingAllowed, createServer, parseAst, parseAstAsync } from 'vite'
4949export type * as Vite from 'vite'
50505151+export { TestCase, TestFile, TestSuite } from '../node/reporters/reported-tasks'
5252+export { TestProject } from '../node/reported-workspace-project'
5353+export type {
5454+ TestCollection,
5555+5656+ TaskOptions,
5757+ TestDiagnostic,
5858+ FileDiagnostic,
5959+ TestResult,
6060+ TestResultPassed,
6161+ TestResultFailed,
6262+ TestResultSkipped,
6363+} from '../node/reporters/reported-tasks'
6464+5165export type {
5266 SequenceHooks,
5367 SequenceSetupFiles,
···6882 UserConfig,
6983 ResolvedConfig,
7084 ProjectConfig,
8585+ ResolvedProjectConfig,
7186 UserWorkspaceConfig,
7287 RuntimeConfig,
7388} from '../node/types/config'
+84
test/cli/fixtures/reported-tasks/1_first.test.ts
···11+import { describe, expect, it } from 'vitest'
22+33+it('runs a test', async () => {
44+ await new Promise(r => setTimeout(r, 10))
55+ expect(1).toBe(1)
66+})
77+88+it('fails a test', async () => {
99+ await new Promise(r => setTimeout(r, 10))
1010+ expect(1).toBe(2)
1111+})
1212+1313+it('fails multiple times', () => {
1414+ expect.soft(1).toBe(2)
1515+ expect.soft(3).toBe(3)
1616+ expect.soft(2).toBe(3)
1717+})
1818+1919+it('skips an option test', { skip: true })
2020+it.skip('skips a .modifier test')
2121+2222+it('todos an option test', { todo: true })
2323+it.todo('todos a .modifier test')
2424+2525+it('retries a test', { retry: 5 }, () => {
2626+ expect(1).toBe(2)
2727+})
2828+2929+let counter = 0
3030+it('retries a test with success', { retry: 5 }, () => {
3131+ expect(counter++).toBe(2)
3232+})
3333+3434+it('repeats a test', { repeats: 5 }, () => {
3535+ expect(1).toBe(2)
3636+})
3737+3838+describe('a group', () => {
3939+ it('runs a test in a group', () => {
4040+ expect(1).toBe(1)
4141+ })
4242+4343+ it('todos an option test in a group', { todo: true })
4444+4545+ describe('a nested group', () => {
4646+ it('runs a test in a nested group', () => {
4747+ expect(1).toBe(1)
4848+ })
4949+5050+ it('fails a test in a nested group', () => {
5151+ expect(1).toBe(2)
5252+ })
5353+5454+ it.concurrent('runs first concurrent test in a nested group', () => {
5555+ expect(1).toBe(1)
5656+ })
5757+5858+ it.concurrent('runs second concurrent test in a nested group', () => {
5959+ expect(1).toBe(1)
6060+ })
6161+ })
6262+})
6363+6464+describe.shuffle('shuffled group', () => {
6565+ it('runs a test in a shuffled group', () => {
6666+ expect(1).toBe(1)
6767+ })
6868+})
6969+7070+describe.each([1])('each group %s', (groupValue) => {
7171+ it.each([2])('each test %s', (itValue) => {
7272+ expect(groupValue + itValue).toBe(3)
7373+ })
7474+})
7575+7676+it('registers a metadata', (ctx) => {
7777+ ctx.task.meta.key = 'value'
7878+})
7979+8080+declare module 'vitest' {
8181+ interface TaskMeta {
8282+ key?: string
8383+ }
8484+}
···11+import type {
22+ Custom as RunnerCustomCase,
33+ Task as RunnerTask,
44+ Test as RunnerTestCase,
55+ File as RunnerTestFile,
66+ Suite as RunnerTestSuite,
77+ TaskMeta,
88+} from '@vitest/runner'
99+import type { TestError } from '@vitest/utils'
1010+import { getTestName } from '../../utils/tasks'
1111+import type { WorkspaceProject } from '../workspace'
1212+import { TestProject } from '../reported-workspace-project'
1313+1414+class ReportedTaskImplementation {
1515+ /**
1616+ * Task instance.
1717+ * @experimental Public runner task API is experimental and does not follow semver.
1818+ */
1919+ public readonly task: RunnerTask
2020+2121+ /**
2222+ * The project assosiacted with the test or suite.
2323+ */
2424+ public readonly project: TestProject
2525+2626+ /**
2727+ * Unique identifier.
2828+ * This ID is deterministic and will be the same for the same test across multiple runs.
2929+ * The ID is based on the file path and test position.
3030+ */
3131+ public readonly id: string
3232+3333+ /**
3434+ * Location in the file where the test or suite is defined.
3535+ */
3636+ public readonly location: { line: number; column: number } | undefined
3737+3838+ protected constructor(
3939+ task: RunnerTask,
4040+ project: WorkspaceProject,
4141+ ) {
4242+ this.task = task
4343+ this.project = project.testProject || (project.testProject = new TestProject(project))
4444+ this.id = task.id
4545+ this.location = task.location
4646+ }
4747+4848+ /**
4949+ * Creates a new reported task instance and stores it in the project's state for future use.
5050+ */
5151+ static register(task: RunnerTask, project: WorkspaceProject) {
5252+ const state = new this(task, project) as TestCase | TestSuite | TestFile
5353+ storeTask(project, task, state)
5454+ return state
5555+ }
5656+}
5757+5858+export class TestCase extends ReportedTaskImplementation {
5959+ #fullName: string | undefined
6060+6161+ declare public readonly task: RunnerTestCase | RunnerCustomCase
6262+ public readonly type: 'test' | 'custom' = 'test'
6363+6464+ /**
6565+ * Direct reference to the test file where the test or suite is defined.
6666+ */
6767+ public readonly file: TestFile
6868+6969+ /**
7070+ * Name of the test.
7171+ */
7272+ public readonly name: string
7373+7474+ /**
7575+ * Options that the test was initiated with.
7676+ */
7777+ public readonly options: TaskOptions
7878+7979+ /**
8080+ * Parent suite. If suite was called directly inside the file, the parent will be the file.
8181+ */
8282+ public readonly parent: TestSuite | TestFile
8383+8484+ protected constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) {
8585+ super(task, project)
8686+8787+ this.name = task.name
8888+ this.file = getReportedTask(project, task.file) as TestFile
8989+ const suite = this.task.suite
9090+ if (suite) {
9191+ this.parent = getReportedTask(project, suite) as TestSuite
9292+ }
9393+ else {
9494+ this.parent = this.file
9595+ }
9696+ this.options = buildOptions(task)
9797+ }
9898+9999+ /**
100100+ * Full name of the test including all parent suites separated with `>`.
101101+ */
102102+ public get fullName(): string {
103103+ if (this.#fullName === undefined) {
104104+ this.#fullName = getTestName(this.task, ' > ')
105105+ }
106106+ return this.#fullName
107107+ }
108108+109109+ /**
110110+ * Result of the test. Will be `undefined` if test is not finished yet or was just collected.
111111+ */
112112+ public result(): TestResult | undefined {
113113+ const result = this.task.result
114114+ if (!result || result.state === 'run') {
115115+ return undefined
116116+ }
117117+ const state = result.state === 'fail'
118118+ ? 'failed'
119119+ : result.state === 'pass'
120120+ ? 'passed'
121121+ : 'skipped'
122122+ return {
123123+ state,
124124+ errors: result.errors as TestError[] | undefined,
125125+ } as TestResult
126126+ }
127127+128128+ /**
129129+ * Checks if the test passed successfully.
130130+ * If the test is not finished yet or was skipped, it will return `true`.
131131+ */
132132+ public ok(): boolean {
133133+ const result = this.result()
134134+ return !result || result.state !== 'failed'
135135+ }
136136+137137+ /**
138138+ * Custom metadata that was attached to the test during its execution.
139139+ */
140140+ public meta(): TaskMeta {
141141+ return this.task.meta
142142+ }
143143+144144+ /**
145145+ * Useful information about the test like duration, memory usage, etc.
146146+ * Diagnostic is only available after the test has finished.
147147+ */
148148+ public diagnostic(): TestDiagnostic | undefined {
149149+ const result = this.task.result
150150+ // startTime should always be available if the test has properly finished
151151+ if (!result || result.state === 'run' || !result.startTime) {
152152+ return undefined
153153+ }
154154+ return {
155155+ heap: result.heap,
156156+ duration: result.duration!,
157157+ startTime: result.startTime,
158158+ retryCount: result.retryCount ?? 0,
159159+ repeatCount: result.repeatCount ?? 0,
160160+ flaky: !!result.retryCount && result.state === 'pass' && result.retryCount > 0,
161161+ }
162162+ }
163163+}
164164+165165+class TestCollection {
166166+ #task: RunnerTestSuite | RunnerTestFile
167167+ #project: WorkspaceProject
168168+169169+ constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) {
170170+ this.#task = task
171171+ this.#project = project
172172+ }
173173+174174+ /**
175175+ * Test or a suite at a specific index in the array.
176176+ */
177177+ at(index: number): TestCase | TestSuite | undefined {
178178+ if (index < 0) {
179179+ index = this.size + index
180180+ }
181181+ return getReportedTask(this.#project, this.#task.tasks[index]) as TestCase | TestSuite | undefined
182182+ }
183183+184184+ /**
185185+ * The number of tests and suites in the collection.
186186+ */
187187+ get size(): number {
188188+ return this.#task.tasks.length
189189+ }
190190+191191+ /**
192192+ * The same collection, but in an array form for easier manipulation.
193193+ */
194194+ array(): (TestCase | TestSuite)[] {
195195+ return Array.from(this)
196196+ }
197197+198198+ /**
199199+ * Iterates over all tests and suites in the collection.
200200+ */
201201+ *values(): IterableIterator<TestCase | TestSuite> {
202202+ return this[Symbol.iterator]()
203203+ }
204204+205205+ /**
206206+ * Filters all tests that are part of this collection's suite and its children.
207207+ */
208208+ *allTests(state?: TestResult['state'] | 'running'): IterableIterator<TestCase> {
209209+ for (const child of this) {
210210+ if (child.type === 'suite') {
211211+ yield * child.children.allTests(state)
212212+ }
213213+ else if (state) {
214214+ const testState = getTestState(child)
215215+ if (state === testState) {
216216+ yield child
217217+ }
218218+ }
219219+ else {
220220+ yield child
221221+ }
222222+ }
223223+ }
224224+225225+ /**
226226+ * Filters only tests that are part of this collection.
227227+ */
228228+ *tests(state?: TestResult['state'] | 'running'): IterableIterator<TestCase> {
229229+ for (const child of this) {
230230+ if (child.type !== 'test') {
231231+ continue
232232+ }
233233+234234+ if (state) {
235235+ const testState = getTestState(child)
236236+ if (state === testState) {
237237+ yield child
238238+ }
239239+ }
240240+ else {
241241+ yield child
242242+ }
243243+ }
244244+ }
245245+246246+ /**
247247+ * Filters only suites that are part of this collection.
248248+ */
249249+ *suites(): IterableIterator<TestSuite> {
250250+ for (const child of this) {
251251+ if (child.type === 'suite') {
252252+ yield child
253253+ }
254254+ }
255255+ }
256256+257257+ /**
258258+ * Filters all suites that are part of this collection's suite and its children.
259259+ */
260260+ *allSuites(): IterableIterator<TestSuite> {
261261+ for (const child of this) {
262262+ if (child.type === 'suite') {
263263+ yield child
264264+ yield * child.children.allSuites()
265265+ }
266266+ }
267267+ }
268268+269269+ *[Symbol.iterator](): IterableIterator<TestSuite | TestCase> {
270270+ for (const task of this.#task.tasks) {
271271+ yield getReportedTask(this.#project, task) as TestSuite | TestCase
272272+ }
273273+ }
274274+}
275275+276276+export type { TestCollection }
277277+278278+abstract class SuiteImplementation extends ReportedTaskImplementation {
279279+ declare public readonly task: RunnerTestSuite | RunnerTestFile
280280+281281+ /**
282282+ * Collection of suites and tests that are part of this suite.
283283+ */
284284+ public readonly children: TestCollection
285285+286286+ protected constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) {
287287+ super(task, project)
288288+ this.children = new TestCollection(task, project)
289289+ }
290290+}
291291+292292+export class TestSuite extends SuiteImplementation {
293293+ #fullName: string | undefined
294294+295295+ declare public readonly task: RunnerTestSuite
296296+ public readonly type = 'suite'
297297+298298+ /**
299299+ * Name of the test or the suite.
300300+ */
301301+ public readonly name: string
302302+303303+ /**
304304+ * Direct reference to the test file where the test or suite is defined.
305305+ */
306306+ public readonly file: TestFile
307307+308308+ /**
309309+ * Parent suite. If suite was called directly inside the file, the parent will be the file.
310310+ */
311311+ public readonly parent: TestSuite | TestFile
312312+313313+ /**
314314+ * Options that suite was initiated with.
315315+ */
316316+ public readonly options: TaskOptions
317317+318318+ protected constructor(task: RunnerTestSuite, project: WorkspaceProject) {
319319+ super(task, project)
320320+321321+ this.name = task.name
322322+ this.file = getReportedTask(project, task.file) as TestFile
323323+ const suite = this.task.suite
324324+ if (suite) {
325325+ this.parent = getReportedTask(project, suite) as TestSuite
326326+ }
327327+ else {
328328+ this.parent = this.file
329329+ }
330330+ this.options = buildOptions(task)
331331+ }
332332+333333+ /**
334334+ * Full name of the suite including all parent suites separated with `>`.
335335+ */
336336+ public get fullName(): string {
337337+ if (this.#fullName === undefined) {
338338+ this.#fullName = getTestName(this.task, ' > ')
339339+ }
340340+ return this.#fullName
341341+ }
342342+}
343343+344344+export class TestFile extends SuiteImplementation {
345345+ declare public readonly task: RunnerTestFile
346346+ declare public readonly location: undefined
347347+ public readonly type = 'file'
348348+349349+ /**
350350+ * This is usually an absolute UNIX file path.
351351+ * It can be a virtual id if the file is not on the disk.
352352+ * This value corresponds to Vite's `ModuleGraph` id.
353353+ */
354354+ public readonly moduleId: string
355355+356356+ protected constructor(task: RunnerTestFile, project: WorkspaceProject) {
357357+ super(task, project)
358358+ this.moduleId = task.filepath
359359+ }
360360+361361+ /**
362362+ * Useful information about the file like duration, memory usage, etc.
363363+ * If the file was not executed yet, all diagnostic values will return `0`.
364364+ */
365365+ public diagnostic(): FileDiagnostic {
366366+ const setupDuration = this.task.setupDuration || 0
367367+ const collectDuration = this.task.collectDuration || 0
368368+ const prepareDuration = this.task.prepareDuration || 0
369369+ const environmentSetupDuration = this.task.environmentLoad || 0
370370+ const duration = this.task.result?.duration || 0
371371+ return {
372372+ environmentSetupDuration,
373373+ prepareDuration,
374374+ collectDuration,
375375+ setupDuration,
376376+ duration,
377377+ }
378378+ }
379379+}
380380+381381+export interface TaskOptions {
382382+ each: boolean | undefined
383383+ concurrent: boolean | undefined
384384+ shuffle: boolean | undefined
385385+ retry: number | undefined
386386+ repeats: number | undefined
387387+ mode: 'run' | 'only' | 'skip' | 'todo'
388388+}
389389+390390+function buildOptions(task: RunnerTestCase | RunnerCustomCase | RunnerTestFile | RunnerTestSuite): TaskOptions {
391391+ return {
392392+ each: task.each,
393393+ concurrent: task.concurrent,
394394+ shuffle: task.shuffle,
395395+ retry: task.retry,
396396+ repeats: task.repeats,
397397+ mode: task.mode,
398398+ }
399399+}
400400+401401+export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped
402402+403403+export interface TestResultPassed {
404404+ /**
405405+ * The test passed successfully.
406406+ */
407407+ state: 'passed'
408408+ /**
409409+ * Errors that were thrown during the test execution.
410410+ *
411411+ * **Note**: If test was retried successfully, errors will still be reported.
412412+ */
413413+ errors: TestError[] | undefined
414414+}
415415+416416+export interface TestResultFailed {
417417+ /**
418418+ * The test failed to execute.
419419+ */
420420+ state: 'failed'
421421+ /**
422422+ * Errors that were thrown during the test execution.
423423+ */
424424+ errors: TestError[]
425425+}
426426+427427+export interface TestResultSkipped {
428428+ /**
429429+ * The test was skipped with `only`, `skip` or `todo` flag.
430430+ * You can see which one was used in the `mode` option.
431431+ */
432432+ state: 'skipped'
433433+ /**
434434+ * Skipped tests have no errors.
435435+ */
436436+ errors: undefined
437437+}
438438+439439+export interface TestDiagnostic {
440440+ /**
441441+ * The amount of memory used by the test in bytes.
442442+ * This value is only available if the test was executed with `logHeapUsage` flag.
443443+ */
444444+ heap: number | undefined
445445+ /**
446446+ * The time it takes to execute the test in ms.
447447+ */
448448+ duration: number
449449+ /**
450450+ * The time in ms when the test started.
451451+ */
452452+ startTime: number
453453+ /**
454454+ * The amount of times the test was retried.
455455+ */
456456+ retryCount: number
457457+ /**
458458+ * The amount of times the test was repeated as configured by `repeats` option.
459459+ * This value can be lower if the test failed during the repeat and no `retry` is configured.
460460+ */
461461+ repeatCount: number
462462+ /**
463463+ * If test passed on a second retry.
464464+ */
465465+ flaky: boolean
466466+}
467467+468468+export interface FileDiagnostic {
469469+ /**
470470+ * The time it takes to import and initiate an environment.
471471+ */
472472+ environmentSetupDuration: number
473473+ /**
474474+ * The time it takes Vitest to setup test harness (runner, mocks, etc.).
475475+ */
476476+ prepareDuration: number
477477+ /**
478478+ * The time it takes to import the test file.
479479+ * This includes importing everything in the file and executing suite callbacks.
480480+ */
481481+ collectDuration: number
482482+ /**
483483+ * The time it takes to import the setup file.
484484+ */
485485+ setupDuration: number
486486+ /**
487487+ * Accumulated duration of all tests and hooks in the file.
488488+ */
489489+ duration: number
490490+}
491491+492492+function getTestState(test: TestCase): TestResult['state'] | 'running' {
493493+ const result = test.result()
494494+ return result ? result.state : 'running'
495495+}
496496+497497+function storeTask(project: WorkspaceProject, runnerTask: RunnerTask, reportedTask: TestCase | TestSuite | TestFile): void {
498498+ project.ctx.state.reportedTasksMap.set(runnerTask, reportedTask)
499499+}
500500+501501+function getReportedTask(project: WorkspaceProject, runnerTask: RunnerTask): TestCase | TestSuite | TestFile {
502502+ const reportedTask = project.ctx.state.getReportedEntity(runnerTask)
503503+ if (!reportedTask) {
504504+ throw new Error(`Task instance was not found for ${runnerTask.type} "${runnerTask.name}"`)
505505+ }
506506+ return reportedTask
507507+}
+15-7
packages/vitest/src/node/types/config.ts
···1010} from '../reporters'
1111import type { TestSequencerConstructor } from '../sequencers/types'
1212import type { ChaiConfig } from '../../integrations/chai/config'
1313-import type { Arrayable, ParsedStack } from '../../types/general'
1313+import type { Arrayable, ErrorWithDiff, ParsedStack } from '../../types/general'
1414import type { JSDOMOptions } from '../../types/jsdom-options'
1515import type { HappyDOMOptions } from '../../types/happy-dom-options'
1616import type { EnvironmentOptions } from '../../types/environment'
···620620 *
621621 * Return `false` to omit the frame.
622622 */
623623- onStackTrace?: (error: Error, frame: ParsedStack) => boolean | void
623623+ onStackTrace?: (error: ErrorWithDiff, frame: ParsedStack) => boolean | void
624624625625 /**
626626 * Indicates if CSS files should be processed.
···10191019 minWorkers: number
10201020}
1021102110221022-export type ProjectConfig = Omit<
10231023- UserConfig,
10241024- | 'sequencer'
10221022+type NonProjectOptions =
10251023 | 'shard'
10261024 | 'watch'
10271025 | 'run'
···10291027 | 'update'
10301028 | 'reporters'
10311029 | 'outputFile'
10321032- | 'poolOptions'
10331030 | 'teardownTimeout'
10341031 | 'silent'
10351032 | 'forceRerunTriggers'
···10471044 | 'slowTestThreshold'
10481045 | 'inspect'
10491046 | 'inspectBrk'
10501050- | 'deps'
10511047 | 'coverage'
10521048 | 'maxWorkers'
10531049 | 'minWorkers'
10541050 | 'fileParallelism'
10511051+10521052+export type ProjectConfig = Omit<
10531053+ UserConfig,
10541054+ NonProjectOptions
10551055+ | 'sequencer'
10561056+ | 'deps'
10571057+ | 'poolOptions'
10551058> & {
10561059 sequencer?: Omit<SequenceOptions, 'sequencer' | 'seed'>
10571060 deps?: Omit<DepsOptions, 'moduleDirectories'>
···10641067 forks?: Pick<NonNullable<PoolOptions['forks']>, 'singleFork' | 'isolate'>
10651068 }
10661069}
10701070+10711071+export type ResolvedProjectConfig = Omit<
10721072+ ResolvedConfig,
10731073+ NonProjectOptions
10741074+>
1067107510681076export type { UserWorkspaceConfig } from '../../public/config'