···2020})
2121```
22222323-Once a test is completed, Vitest will send a task including the result and `meta` to the Node.js process using RPC. To intercept and process this task, you can utilize the `onTaskUpdate` method available in your reporter implementation:
2323+Once a test is completed, Vitest will send a task including the result and `meta` to the Node.js process using RPC, and then report it in `onTestCaseResult` and other hooks that have access to tasks. To process this test case, you can utilize the `onTestCaseResult` method available in your reporter implementation:
24242525```ts [custom-reporter.js]
2626-export default {
2727- // you can intercept packs if needed
2828- onTaskUpdate(packs) {
2929- const [id, result, meta] = packs[0]
3030- },
3131- // meta is located on every task inside "onFinished"
3232- onFinished(files) {
3333- files[0].meta.done === true
3434- files[0].tasks[0].meta.custom === 'some-custom-handler'
3535- }
3636-}
3737-```
2626+import type { Reporter, TestCase, TestModule } from 'vitest/node'
38273939-::: warning
4040-Vitest can send several tasks at the same time if several tests are completed in a short period of time.
4141-:::
2828+export default {
2929+ onTestCaseResult(testCase: TestCase) {
3030+ // custom === 'some-custom-handler' ✅
3131+ const { custom } = testCase.meta()
3232+ },
3333+ onTestRunEnd(testModule: TestModule) {
3434+ testModule.meta().done === true
3535+ testModule.children.at(0).meta().custom === 'some-custom-handler'
3636+ }
3737+} satisfies Reporter
3838+```
42394340::: danger BEWARE
4441Vitest uses different methods to communicate with the Node.js process.
···56535754```ts
5855const vitest = await createVitest('test')
5959-await vitest.start()
6060-vitest.state.getFiles()[0].meta.done === true
6161-vitest.state.getFiles()[0].tasks[0].meta.custom === 'some-custom-handler'
5656+const { testModules } = await vitest.start()
5757+5858+const testModule = testModules[0]
5959+testModule.meta().done === true
6060+testModule.children.at(0).meta().custom === 'some-custom-handler'
6261```
63626463It's also possible to extend type definitions when using TypeScript:
+10
docs/api/index.md
···1179117911801180::: tip
11811181This hook is always called in reverse order and is not affected by [`sequence.hooks`](/config/#sequence-hooks) option.
11821182+11831183+<!-- TODO: should it be called? https://github.com/vitest-dev/vitest/pull/7069 -->
11841184+Note that this hook is not called if test was skipped with a dynamic `ctx.skip()` call:
11851185+11861186+```ts{2}
11871187+test('skipped dynamically', (t) => {
11881188+ onTestFinished(() => {}) // not called
11891189+ t.skip()
11901190+})
11911191+```
11821192:::
1183119311841194### onTestFailed
+311
docs/advanced/api/reporters.md
···11+# Reporters
22+33+::: warning
44+This is an advanced API. If you just want to configure built-in reporters, read the ["Reporters"](/guide/reporters) guide.
55+:::
66+77+Vitest has its own test run lifecycle. These are represented by reporter's methods:
88+99+- [`onInit`](#oninit)
1010+- [`onTestRunStart`](#ontestrunstart)
1111+ - [`onTestModuleQueued`](#ontestmodulequeued)
1212+ - [`onTestModuleCollected`](#ontestmodulecollected)
1313+ - [`onTestModuleStart`](#ontestmodulestart)
1414+ - [`onTestSuiteReady`](#ontestsuiteready)
1515+ - [`onHookStart(beforeAll)`](#onhookstart)
1616+ - [`onHookEnd(beforeAll)`](#onhookend)
1717+ - [`onTestCaseReady`](#ontestcaseready)
1818+ - [`onHookStart(beforeEach)`](#onhookstart)
1919+ - [`onHookEnd(beforeEach)`](#onhookend)
2020+ - [`onHookStart(afterEach)`](#onhookstart)
2121+ - [`onHookEnd(afterEach)`](#onhookend)
2222+ - [`onTestCaseResult`](#ontestcaseresult)
2323+ - [`onHookStart(afterAll)`](#onhookstart)
2424+ - [`onHookEnd(afterAll)`](#onhookend)
2525+ - [`onTestSuiteResult`](#ontestsuiteresult)
2626+ - [`onTestModuleEnd`](#ontestmoduleend)
2727+- [`onTestRunEnd`](#ontestrunend)
2828+2929+Tests and suites within a single module will be reported in order unless they were skipped. All skipped tests are reported at the end of suite/module.
3030+3131+Note that since test modules can run in parallel, Vitest will report them in parallel.
3232+3333+This guide lists all supported reporter methods. However, don't forget that instead of creating your own reporter, you can [extend existing one](/advanced/reporters) instead:
3434+3535+```ts [custom-reporter.js]
3636+import { BaseReporter } from 'vitest/reporters'
3737+3838+export default class CustomReporter extends BaseReporter {
3939+ onTestRunEnd(testModules, errors) {
4040+ console.log(testModule.length, 'tests finished running')
4141+ super.onTestRunEnd(testModules, errors)
4242+ }
4343+}
4444+```
4545+4646+## onInit
4747+4848+```ts
4949+function onInit(vitest: Vitest): Awaitable<void>
5050+```
5151+5252+This method is called when [Vitest](/advanced/api/vitest) was initiated or started, but before the tests were filtered.
5353+5454+::: info
5555+Internally this method is called inside [`vitest.start`](/advanced/api/vitest#start), [`vitest.init`](/advanced/api/vitest#init) or [`vitest.mergeReports`](/advanced/api/vitest#mergereports). If you are using programmatic API, make sure to call either one dependning on your needs before calling [`vitest.runTestSpecifications`](/advanced/api/vitest#runtestspecifications), for example. Built-in CLI will always run methods in correct order.
5656+:::
5757+5858+Note that you can also get access to `vitest` instance from test cases, suites and test modules via a [`project`](/advanced/api/test-project) property, but it might also be useful to store a reference to `vitest` in this method.
5959+6060+::: details Example
6161+```ts
6262+import type { Reporter, TestSpecification, Vitest } from 'vitest/node'
6363+6464+class MyReporter implements Reporter {
6565+ private vitest!: Vitest
6666+6767+ onInit(vitest: Vitest) {
6868+ this.vitest = vitest
6969+ }
7070+7171+ onTestRunStart(specifications: TestSpecification[]) {
7272+ console.log(
7373+ specifications.length,
7474+ 'test files will run in',
7575+ this.vitest.config.root,
7676+ )
7777+ }
7878+}
7979+8080+export default new MyReporter()
8181+```
8282+:::
8383+8484+## onTestRunStart
8585+8686+```ts
8787+function onTestRunStart(
8888+ specifications: TestSpecification[]
8989+): Awaitable<void>
9090+```
9191+9292+This method is called when a new test run has started. It receives an array of [test specifications](/advanced/api/test-specification) scheduled to run. This array is readonly and available only for information purposes.
9393+9494+If Vitest didn't find any test files to run, this event will be invoked with an empty array, and then [`onTestRunEnd`](#ontestrunend) will be called immediately after.
9595+9696+::: details Example
9797+```ts
9898+import type { Reporter, TestSpecification } from 'vitest/node'
9999+100100+class MyReporter implements Reporter {
101101+ onTestRunStart(specifications: TestSpecification[]) {
102102+ console.log(specifications.length, 'test files will run')
103103+ }
104104+}
105105+106106+export default new MyReporter()
107107+```
108108+:::
109109+110110+::: tip DEPRECATION NOTICE
111111+This method was added in Vitest 3, replacing `onPathsCollected` and `onSpecsCollected`, both of which are now deprecated.
112112+:::
113113+114114+## onTestRunEnd
115115+116116+```ts
117117+function onTestRunEnd(
118118+ testModules: ReadonlyArray<TestModule>,
119119+ unhandledErrors: ReadonlyArray<SerializedError>,
120120+ reason: TestRunEndReason
121121+): Awaitable<void>
122122+```
123123+124124+This method is called after all tests have finished running and the coverage merged all reports, if it's enabled. Note that you can get the coverage information in [`onCoverage`](#oncoverage) hook.
125125+126126+It receives a readonly list of test modules. You can iterate over it via a [`testModule.children`](/advanced/api/test-collection) property to report the state and errors, if any.
127127+128128+The second argument is a readonly list of unhandled errors that Vitest wasn't able to attribute to any test. These can happen outside of the test run because of an error in a plugin, or inside the test run as a side-effect of a non-awaited function (for example, a timeout that threw an error after the test has finished running).
129129+130130+The third argument indicated why the test run was finished:
131131+132132+- `passed`: test run was finished normally and there are no errors
133133+- `failed`: test run has at least one error (due to a syntax error during collection or an actual error during test execution)
134134+- `interrupted`: test was interruped by [`vitest.cancelCurrentRun`](/advanced/api/vitest#cancelcurrentrun) call or `Ctrl+C` was pressed in the terminal (note that it's still possible to have failed tests in this case)
135135+136136+If Vitest didn't find any test files to run, this event will be invoked with empty arrays of modules and errors, and the state will depend on the value of [`config.passWithNoTests`](/config/#passwithnotests).
137137+138138+::: details Example
139139+```ts
140140+import type {
141141+ Reporter,
142142+ SerializedError,
143143+ TestModule,
144144+ TestRunEndReason,
145145+ TestSpecification
146146+} from 'vitest/node'
147147+148148+class MyReporter implements Reporter {
149149+ onTestRunEnd(
150150+ testModules: ReadonlyArray<TestModule>,
151151+ unhandledErrors: ReadonlyArray<SerializedError>,
152152+ reason: TestRunEndReason,
153153+ ) {
154154+ if (reason === 'passed') {
155155+ testModules.forEach(module => console.log(module.moduleId, 'succeeded'))
156156+ }
157157+ else if (reason === 'failed') {
158158+ // note that this will skip possible errors in suites
159159+ // you can get them from testSuite.errors()
160160+ for (const testCase of testModules.children.allTests()) {
161161+ if (testCase.result().state === 'failed') {
162162+ console.log(testCase.fullName, 'in', testCase.module.moduleId, 'failed')
163163+ console.log(testCase.result().errors)
164164+ }
165165+ }
166166+ }
167167+ else {
168168+ console.log('test run was interrupted, skipping report')
169169+ }
170170+ }
171171+}
172172+173173+export default new MyReporter()
174174+```
175175+:::
176176+177177+::: tip DEPRECATION NOTICE
178178+This method was added in Vitest 3, replacing `onFinished`, which is now deprecated.
179179+:::
180180+181181+## onCoverage
182182+183183+```ts
184184+function onCoverage(coverage: unknown): Awaitable<void>
185185+```
186186+187187+This hook is called after coverage results have been processed. Coverage provider's reporters are called after this hook. The typings of `coverage` depends on the `coverage.provider`. For Vitest's default built-in providers you can import the types from `istanbul-lib-coverage` package:
188188+189189+```ts
190190+import type { CoverageMap } from 'istanbul-lib-coverage'
191191+192192+declare function onCoverage(coverage: CoverageMap): Awaitable<void>
193193+```
194194+195195+If Vitest didn't perform any coverage, this hook is not called.
196196+197197+## onTestModuleQueued
198198+199199+```ts
200200+function onTestModuleQueued(testModule: TestModule): Awaitable<void>
201201+```
202202+203203+This method is called right before Vitest imports the setup file and the test module itself. This means that `testModule` will have no [`children`](/advanced/api/test-suite#children) yet, but you can start reporting it as the next test to run.
204204+205205+## onTestModuleCollected
206206+207207+```ts
208208+function onTestModuleCollected(testModule: TestModule): Awaitable<void>
209209+```
210210+211211+This method is called when all tests inside the file were collected, meaning [`testModule.children`](/advanced/api/test-suite#children) collection is populated, but tests don't have any results yet.
212212+213213+## onTestModuleStart
214214+215215+```ts
216216+function onTestModuleStart(testModule: TestModule): Awaitable<void>
217217+```
218218+219219+This method is called right after [`onTestModuleCollected`](#ontestmodulecollected) unless Vitest runs in collection mode ([`vitest.collect()`](/advanced/api/vitest#collect) or `vitest collect` in the CLI), in this case it will not be called at all because there are no tests to run.
220220+221221+## onTestModuleEnd
222222+223223+```ts
224224+function onTestModuleEnd(testModule: TestModule): Awaitable<void>
225225+```
226226+227227+This method is called when every test in the module finished running. This means, every test inside [`testModule.children`](/advanced/api/test-suite#children) will have a `test.result()` that is not equal to `pending`.
228228+229229+## onHookStart
230230+231231+```ts
232232+function onHookStart(context: ReportedHookContext): Awaitable<void>
233233+```
234234+235235+This method is called when any of these hooks have started running:
236236+237237+- `beforeAll`
238238+- `afterAll`
239239+- `beforeEach`
240240+- `afterEach`
241241+242242+If `beforeAll` or `afterAll` are started, the `entity` will be either [`TestSuite`](/advanced/api/test-suite) or [`TestModule`](/advanced/api/test-module).
243243+244244+If `beforeEach` or `afterEach` are started, the `entity` will always be [`TestCase`](/advanced/api/test-case).
245245+246246+::: warning
247247+`onHookStart` method will not be called if the hook did not run during the test run.
248248+:::
249249+250250+## onHookEnd
251251+252252+```ts
253253+function onHookEnd(context: ReportedHookContext): Awaitable<void>
254254+```
255255+256256+This method is called when any of these hooks have finished running:
257257+258258+- `beforeAll`
259259+- `afterAll`
260260+- `beforeEach`
261261+- `afterEach`
262262+263263+If `beforeAll` or `afterAll` have finished, the `entity` will be either [`TestSuite`](/advanced/api/test-suite) or [`TestModule`](/advanced/api/test-module).
264264+265265+If `beforeEach` or `afterEach` have finished, the `entity` will always be [`TestCase`](/advanced/api/test-case).
266266+267267+::: warning
268268+`onHookEnd` method will not be called if the hook did not run during the test run.
269269+:::
270270+271271+## onTestSuiteReady
272272+273273+```ts
274274+function onTestSuiteReady(testSuite: TestSuite): Awaitable<void>
275275+```
276276+277277+This method is called before the suite starts to run its tests. This method is also called if the suite was skipped.
278278+279279+If the file doesn't have any suites, this method will not be called. Consider using `onTestModuleStart` to cover this use case.
280280+281281+## onTestSuiteResult
282282+283283+```ts
284284+function onTestSuiteResult(testSuite: TestSuite): Awaitable<void>
285285+```
286286+287287+This method is called after the suite has finished running tests. This method is also called if the suite was skipped.
288288+289289+If the file doesn't have any suites, this method will not be called. Consider using `onTestModuleEnd` to cover this use case.
290290+291291+## onTestCaseReady
292292+293293+```ts
294294+function onTestCaseReady(testCase: TestCase): Awaitable<void>
295295+```
296296+297297+This method is called before the test starts to run or it was skipped. Note that `beforeEach` and `afterEach` hooks are considered part of the test because they can influence the result.
298298+299299+::: warning
300300+Notice that it's possible to have [`testCase.result()`](/advanced/api/test-case#result) with `passed` or `failed` state already when `onTestCaseReady` is called. This can happen if test was running too fast and both `onTestCaseReady` and `onTestCaseResult` were scheduled to run in the same microtask.
301301+:::
302302+303303+## onTestCaseResult
304304+305305+```ts
306306+function onTestCaseResult(testCase: TestCase): Awaitable<void>
307307+```
308308+309309+This method is called when the test has finished running or was just skipped. Note that this will be called after the `afterEach` hook is finished, if there are any.
310310+311311+At this point, [`testCase.result()`](/advanced/api/test-case#result) will have non-pending state.
+41-56
docs/advanced/api/test-case.md
···1010}
1111```
12121313-::: warning
1414-We are planning to introduce a new Reporter API that will be using this API by default. For now, the Reporter API uses [runner tasks](/advanced/runner#tasks), but you can still access `TestCase` via `vitest.state.getReportedEntity` method:
1515-1616-```ts
1717-import type { RunnerTestFile, TestModule, Vitest } from 'vitest/node'
1818-1919-class Reporter {
2020- private vitest!: Vitest
2121-2222- onInit(vitest: Vitest) {
2323- this.vitest = vitest
2424- }
2525-2626- onFinished(files: RunnerTestFile[]) {
2727- for (const file of files) {
2828- const testModule = this.vitest.getReportedEntity(file) as TestModule
2929- for (const test of testModule.children.allTests()) {
3030- console.log(test) // TestCase
3131- }
3232- }
3333- }
3434-}
3535-```
3636-:::
3737-3813## project
39144015This references the [`TestProject`](/advanced/api/test-project) that the test belongs to.
···12499125100```ts
126101interface TaskOptions {
127127- each: boolean | undefined
128128- concurrent: boolean | undefined
129129- shuffle: boolean | undefined
130130- retry: number | undefined
131131- repeats: number | undefined
132132- mode: 'run' | 'only' | 'skip' | 'todo'
102102+ readonly each: boolean | undefined
103103+ readonly fails: boolean | undefined
104104+ readonly concurrent: boolean | undefined
105105+ readonly shuffle: boolean | undefined
106106+ readonly retry: number | undefined
107107+ readonly repeats: number | undefined
108108+ readonly mode: 'run' | 'only' | 'skip' | 'todo'
133109}
134110```
135111···142118```
143119144120Checks if the test did not fail the suite. If the test is not finished yet or was skipped, it will return `true`.
145145-146146-## skipped
147147-148148-```ts
149149-function skipped(): boolean
150150-```
151151-152152-Checks if the test was skipped during collection or dynamically with `ctx.skip()`.
153121154122## meta
155123···174142## result
175143176144```ts
177177-function result(): TestResult | undefined
145145+function result(): TestResult
178146```
179147180180-Test results. It will be `undefined` if test is skipped during collection, not finished yet or was just collected.
148148+Test results. If test is not finished yet or was just collected, it will be equal to `TestResultPending`:
149149+150150+```ts
151151+export interface TestResultPending {
152152+ /**
153153+ * The test was collected, but didn't finish running yet.
154154+ */
155155+ readonly state: 'pending'
156156+ /**
157157+ * Pending tests have no errors.
158158+ */
159159+ readonly errors: undefined
160160+}
161161+```
181162182163If the test was skipped, the return value will be `TestResultSkipped`:
183164···187168 * The test was skipped with `skip` or `todo` flag.
188169 * You can see which one was used in the `options.mode` option.
189170 */
190190- state: 'skipped'
171171+ readonly state: 'skipped'
191172 /**
192173 * Skipped tests have no errors.
193174 */
194194- errors: undefined
175175+ readonly errors: undefined
195176 /**
196177 * A custom note passed down to `ctx.skip(note)`.
197178 */
198198- note: string | undefined
179179+ readonly note: string | undefined
199180}
200181```
201182···210191 /**
211192 * The test failed to execute.
212193 */
213213- state: 'failed'
194194+ readonly state: 'failed'
214195 /**
215196 * Errors that were thrown during the test execution.
216197 */
217217- errors: TestError[]
198198+ readonly errors: ReadonlyArray<TestError>
218199}
219200```
220201221221-If the test passed, the retunr value will be `TestResultPassed`:
202202+If the test passed, the return value will be `TestResultPassed`:
222203223204```ts
224205interface TestResultPassed {
225206 /**
226207 * The test passed successfully.
227208 */
228228- state: 'passed'
209209+ readonly state: 'passed'
229210 /**
230211 * Errors that were thrown during the test execution.
231212 */
232232- errors: TestError[] | undefined
213213+ readonly errors: ReadonlyArray<TestError> | undefined
233214}
234215```
235216···250231 /**
251232 * If the duration of the test is above `slowTestThreshold`.
252233 */
253253- slow: boolean
234234+ readonly slow: boolean
254235 /**
255236 * The amount of memory used by the test in bytes.
256237 * This value is only available if the test was executed with `logHeapUsage` flag.
257238 */
258258- heap: number | undefined
239239+ readonly heap: number | undefined
259240 /**
260241 * The time it takes to execute the test in ms.
261242 */
262262- duration: number
243243+ readonly duration: number
263244 /**
264245 * The time in ms when the test started.
265246 */
266266- startTime: number
247247+ readonly startTime: number
267248 /**
268249 * The amount of times the test was retried.
269250 */
270270- retryCount: number
251251+ readonly retryCount: number
271252 /**
272253 * The amount of times the test was repeated as configured by `repeats` option.
273254 * This value can be lower if the test failed during the repeat and no `retry` is configured.
274255 */
275275- repeatCount: number
256256+ readonly repeatCount: number
276257 /**
277258 * If test passed on a second retry.
278259 */
279279- flaky: boolean
260260+ readonly flaky: boolean
280261}
281262```
263263+264264+::: info
265265+`diagnostic()` will return `undefined` if the test was not scheduled to run yet.
266266+:::
+3-7
docs/advanced/api/test-collection.md
···5757## allTests
58585959```ts
6060-function allTests(
6161- state?: TestResult['state'] | 'running'
6262-): Generator<TestCase, undefined, void>
6060+function allTests(state?: TestState): Generator<TestCase, undefined, void>
6361```
64626563Filters all tests that are part of this collection and its children.
66646765```ts
6866for (const test of module.children.allTests()) {
6969- if (!test.result()) {
6767+ if (test.result().state === 'pending') {
7068 console.log('test', test.fullName, 'did not finish')
7169 }
7270}
···7775## tests
78767977```ts
8080-function tests(
8181- state?: TestResult['state'] | 'running'
8282-): Generator<TestCase, undefined, void>
7878+function tests(state?: TestState): Generator<TestCase, undefined, void>
8379```
84808581Filters only the tests that are part of this collection. You can pass down a `state` value to filter tests by the state.
+21-28
docs/advanced/api/test-module.md
···1010}
1111```
12121313-The `TestModule` inherits all methods and properties from the [`TestSuite`](/advanced/api/test-module). This guide will only list methods and properties unique to the `TestModule`
1414-1515-::: warning
1616-We are planning to introduce a new Reporter API that will be using this API by default. For now, the Reporter API uses [runner tasks](/advanced/runner#tasks), but you can still access `TestModule` via `vitest.state.getReportedEntity` method:
1717-1818-```ts
1919-import type { RunnerTestFile, TestModule, Vitest } from 'vitest/node'
2020-2121-class Reporter {
2222- private vitest!: Vitest
2323-2424- onInit(vitest: Vitest) {
2525- this.vitest = vitest
2626- }
2727-2828- onFinished(files: RunnerTestFile[]) {
2929- for (const file of files) {
3030- const testModule = this.vitest.state.getReportedEntity(file) as TestModule
3131- console.log(testModule) // TestModule
3232- }
3333- }
3434-}
3535-```
1313+::: warning Extending Suite Methods
1414+The `TestModule` class inherits all methods and properties from the [`TestSuite`](/advanced/api/test-suite). This guide will only list methods and properties unique to the `TestModule`.
3615:::
37163817## moduleId
39184019This is usually an absolute unix file path (even on Windows). It can be a virtual id if the file is not on the disk. This value corresponds to Vite's `ModuleGraph` id.
2020+2121+```ts
2222+'C:/Users/Documents/project/example.test.ts' // ✅
2323+'/Users/mac/project/example.test.ts' // ✅
2424+'C:\\Users\\Documents\\project\\example.test.ts' // ❌
2525+```
2626+2727+## state
2828+2929+```ts
3030+function state(): TestModuleState
3131+```
3232+3333+Works the same way as [`testSuite.state()`](/advanced/api/test-suite#state), but can also return `queued` if module wasn't executed yet.
41344235## diagnostic
4336···5245 /**
5346 * The time it takes to import and initiate an environment.
5447 */
5555- environmentSetupDuration: number
4848+ readonly environmentSetupDuration: number
5649 /**
5750 * The time it takes Vitest to setup test harness (runner, mocks, etc.).
5851 */
5959- prepareDuration: number
5252+ readonly prepareDuration: number
6053 /**
6154 * The time it takes to import the test module.
6255 * This includes importing everything in the module and executing suite callbacks.
6356 */
6464- collectDuration: number
5757+ readonly collectDuration: number
6558 /**
6659 * The time it takes to import the setup module.
6760 */
6868- setupDuration: number
6161+ readonly setupDuration: number
6962 /**
7063 * Accumulated duration of all tests and hooks in the module.
7164 */
7272- duration: number
6565+ readonly duration: number
7366}
7467```
+8
docs/advanced/api/test-specification.md
···13131414`createSpecification` expects resolved module ID. It doesn't auto-resolve the file or check that it exists on the file system.
15151616+## taskId
1717+1818+[Test module's](/advanced/api/test-suite#id) identifier.
1919+1620## project
17211822This references the [`TestProject`](/advanced/api/test-project) that the test module belongs to.
···2630'/Users/mac/project/example.test.ts' // ✅
2731'C:\\Users\\Documents\\project\\example.test.ts' // ❌
2832```
3333+3434+## testModule
3535+3636+Instance of [`TestModule`](/advanced/api/test-module) assosiated with the specification. If test wasn't queued yet, this will be `undefined`.
29373038## pool <Badge type="warning">experimental</Badge> {#pool}
3139
+35-36
docs/advanced/api/test-suite.md
···1010}
1111```
12121313-::: warning
1414-We are planning to introduce a new Reporter API that will be using this API by default. For now, the Reporter API uses [runner tasks](/advanced/runner#tasks), but you can still access `TestSuite` via `vitest.state.getReportedEntity` method:
1515-1616-```ts
1717-import type { RunnerTestFile, TestModule, Vitest } from 'vitest/node'
1818-1919-class Reporter {
2020- private vitest!: Vitest
2121-2222- onInit(vitest: Vitest) {
2323- this.vitest = vitest
2424- }
2525-2626- onFinished(files: RunnerTestFile[]) {
2727- for (const file of files) {
2828- const testModule = this.vitest.state.getReportedEntity(file) as TestModule
2929- for (const suite of testModule.children.allSuites()) {
3030- console.log(suite) // TestSuite
3131- }
3232- }
3333- }
3434-}
3535-```
3636-:::
3737-3813## project
39144015This references the [`TestProject`](/advanced/api/test-project) that the test belongs to.
···125100126101```ts
127102interface TaskOptions {
128128- each: boolean | undefined
129129- concurrent: boolean | undefined
130130- shuffle: boolean | undefined
131131- retry: number | undefined
132132- repeats: number | undefined
133133- mode: 'run' | 'only' | 'skip' | 'todo'
103103+ readonly each: boolean | undefined
104104+ readonly fails: boolean | undefined
105105+ readonly concurrent: boolean | undefined
106106+ readonly shuffle: boolean | undefined
107107+ readonly retry: number | undefined
108108+ readonly repeats: number | undefined
109109+ readonly mode: 'run' | 'only' | 'skip' | 'todo'
134110}
135111```
136112···153129```
154130155131::: warning
156156-Note that `suite.children` will only iterate the first level of nesting, it won't go deeper.
132132+Note that `suite.children` will only iterate the first level of nesting, it won't go deeper. If you need to iterate over all tests or suites, use [`children.allTests()`](/advanced/api/test-collection#alltests) or [`children.allSuites()`](/advanced/api/test-collection#allsuites). If you need to iterate over everything, use recursive function:
133133+134134+```ts
135135+function visit(collection: TestCollection) {
136136+ for (const task of collection) {
137137+ if (task.type === 'suite') {
138138+ // report a suite
139139+ visit(task.children)
140140+ }
141141+ else {
142142+ // report a test
143143+ }
144144+ }
145145+}
146146+```
157147:::
158148159149## ok
···164154165155Checks if the suite has any failed tests. This will also return `false` if suite failed during collection. In that case, check the [`errors()`](#errors) for thrown errors.
166156167167-## skipped
157157+## state
168158169159```ts
170170-function skipped(): boolean
160160+function state(): TestSuiteState
171161```
172162173173-Checks if the suite was skipped during collection.
163163+Checks the running state of the suite. Possible return values:
164164+165165+- **pending**: the tests in this suite did not finish running yet.
166166+- **failed**: this suite has failed tests or they couldn't be collected. If [`errors()`](#errors) is not empty, it means the suite failed to collect tests.
167167+- **passed**: every test inside this suite has passed.
168168+- **skipped**: this suite was skipped during collection.
169169+170170+::: warning
171171+Note that [test module](/advanced/api/test-module) also has a `state` method that returns the same values, but it can also return an additional `queued` state if the module wasn't executed yet.
172172+:::
174173175174## errors
176175···189188```
190189191190::: warning
192192-Note that errors are serialized into simple object: `instanceof Error` will always return `false`.
191191+Note that errors are serialized into simple objects: `instanceof Error` will always return `false`.
193192:::
···160160 *
161161 * **Note:** The `onTestFinished` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file.
162162 *
163163+ * **Note:** The `onTestFinished` hook is not called if the test is canceled with a dynamic `ctx.skip()` call.
164164+ *
163165 * @param {Function} fn - The callback function to be executed after a test finishes. The function can receive parameters providing details about the completed test, including its success or failure status.
164166 * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
165167 * @throws {Error} Throws an error if the function is not called within a test.
···55 SequenceSetupFiles,
66 Suite,
77 Task,
88+ TaskEventPack,
89 TaskResultPack,
910 Test,
1011 TestContext,
···128129 /**
129130 * Called, when a task is updated. The same as "onTaskUpdate" in a reporter, but this is running in the same thread as tests.
130131 */
131131- onTaskUpdate?: (task: TaskResultPack[]) => Promise<void>
132132+ onTaskUpdate?: (task: TaskResultPack[], events: TaskEventPack[]) => Promise<void>
132133133134 /**
134135 * Called before running all tests in collected paths.
+29-4
packages/runner/src/types/tasks.ts
···7979 */
8080 file: File
8181 /**
8282- * Whether the task was skipped by calling `t.skip()`.
8383- */
8484- pending?: boolean
8585- /**
8682 * Whether the task should succeed if it fails. If the task fails, it will be marked as passed.
8783 */
8884 fails?: boolean
···152148 repeatCount?: number
153149 /** @private */
154150 note?: string
151151+ /**
152152+ * Whether the task was skipped by calling `t.skip()`.
153153+ * @internal
154154+ */
155155+ pending?: boolean
155156}
156157157158/**
···172173 */
173174 meta: TaskMeta,
174175]
176176+177177+export type TaskEventPack = [
178178+ /**
179179+ * Unique task identifier from `task.id`.
180180+ */
181181+ id: string,
182182+ /**
183183+ * The name of the event that triggered the update.
184184+ */
185185+ event: TaskUpdateEvent,
186186+]
187187+188188+export type TaskUpdateEvent =
189189+ | 'test-failed-early'
190190+ | 'suite-failed-early'
191191+ | 'test-prepare'
192192+ | 'test-finished'
193193+ | 'test-retried'
194194+ | 'suite-prepare'
195195+ | 'suite-finished'
196196+ | 'before-hook-start'
197197+ | 'before-hook-end'
198198+ | 'after-hook-start'
199199+ | 'after-hook-end'
175200176201export interface Suite extends TaskBase {
177202 type: 'suite'
···11-import type { File, TaskResultPack, TaskState, Test } from '@vitest/runner'
11+import type { File, Task, Test } from '@vitest/runner'
22import type { Vitest } from '../core'
33-import { getTests } from '@vitest/runner/utils'
33+import type { TestCase, TestModule } from './reported-tasks'
44import c from 'tinyrainbow'
55import { BaseReporter } from './base'
66import { WindowRenderer } from './renderers/windowedRenderer'
77-import { TaskParser } from './task-parser'
8798interface Icon {
109 char: string
1110 color: (char: string) => string
1211}
13121313+type TestCaseState = ReturnType<TestCase['result']>['state']
1414+1415export class DotReporter extends BaseReporter {
1515- private summary?: DotSummary
1616+ private renderer?: WindowRenderer
1717+ private tests = new Map<Test['id'], TestCaseState>()
1818+ private finishedTests = new Set<TestCase['id']>()
16191720 onInit(ctx: Vitest) {
1821 super.onInit(ctx)
19222023 if (this.isTTY) {
2121- this.summary = new DotSummary()
2222- this.summary.onInit(ctx)
2424+ this.renderer = new WindowRenderer({
2525+ logger: ctx.logger,
2626+ getWindow: () => this.createSummary(),
2727+ })
2828+2929+ this.ctx.onClose(() => this.renderer?.stop())
2330 }
2431 }
25322626- onTaskUpdate(packs: TaskResultPack[]) {
2727- this.summary?.onTaskUpdate(packs)
2828-3333+ printTask(task: Task) {
2934 if (!this.isTTY) {
3030- super.onTaskUpdate(packs)
3535+ super.printTask(task)
3136 }
3237 }
33383439 onWatcherRerun(files: string[], trigger?: string) {
3535- this.summary?.onWatcherRerun()
4040+ this.tests.clear()
4141+ this.renderer?.start()
3642 super.onWatcherRerun(files, trigger)
3743 }
38443945 onFinished(files?: File[], errors?: unknown[]) {
4040- this.summary?.onFinished()
4646+ if (this.isTTY) {
4747+ const finalLog = formatTests(Array.from(this.tests.values()))
4848+ this.ctx.logger.log(finalLog)
4949+ }
5050+5151+ this.tests.clear()
5252+ this.renderer?.finish()
5353+4154 super.onFinished(files, errors)
4255 }
4343-}
44564545-class DotSummary extends TaskParser {
4646- private renderer!: WindowRenderer
4747- private tests = new Map<Test['id'], TaskState>()
4848- private finishedTests = new Set<Test['id']>()
4949-5050- onInit(ctx: Vitest): void {
5151- this.ctx = ctx
5252-5353- this.renderer = new WindowRenderer({
5454- logger: ctx.logger,
5555- getWindow: () => this.createSummary(),
5656- })
5757-5858- this.ctx.onClose(() => this.renderer.stop())
5959- }
6060-6161- onWatcherRerun() {
6262- this.tests.clear()
6363- this.renderer.start()
6464- }
6565-6666- onFinished() {
6767- const finalLog = formatTests(Array.from(this.tests.values()))
6868- this.ctx.logger.log(finalLog)
6969-7070- this.tests.clear()
7171- this.renderer.finish()
7272- }
7373-7474- onTestFilePrepare(file: File): void {
7575- for (const test of getTests(file)) {
5757+ onTestModuleCollected(module: TestModule): void {
5858+ for (const test of module.children.allTests()) {
7659 // Dot reporter marks pending tests as running
7777- this.onTestStart(test)
6060+ this.onTestCaseReady(test)
7861 }
7962 }
80638181- onTestStart(test: Test) {
6464+ onTestCaseReady(test: TestCase) {
8265 if (this.finishedTests.has(test.id)) {
8366 return
8467 }
8585-8686- this.tests.set(test.id, test.mode || 'run')
6868+ this.tests.set(test.id, test.result().state || 'run')
8769 }
88708989- onTestFinished(test: Test) {
9090- if (this.finishedTests.has(test.id)) {
9191- return
9292- }
9393-7171+ onTestCaseResult(test: TestCase) {
9472 this.finishedTests.add(test.id)
9595- this.tests.set(test.id, test.result?.state || 'skip')
7373+ this.tests.set(test.id, test.result().state || 'skipped')
9674 }
97759898- onTestFileFinished() {
7676+ onTestModuleEnd() {
7777+ if (!this.isTTY) {
7878+ return
7979+ }
8080+9981 const columns = this.ctx.logger.getColumns()
1008210183 if (this.tests.size < columns) {
10284 return
10385 }
10486105105- const finishedTests = Array.from(this.tests).filter(entry => entry[1] !== 'run')
8787+ const finishedTests = Array.from(this.tests).filter(entry => entry[1] !== 'pending')
1068810789 if (finishedTests.length < columns) {
10890 return
10991 }
1109211193 // Remove finished tests from state and render them in static output
112112- const states: TaskState[] = []
9494+ const states: TestCaseState[] = []
11395 let count = 0
1149611597 for (const [id, state] of finishedTests) {
···138120const pending: Icon = { char: '*', color: c.yellow }
139121const skip: Icon = { char: '-', color: (char: string) => c.dim(c.gray(char)) }
140122141141-function getIcon(state: TaskState): Icon {
123123+function getIcon(state: TestCaseState): Icon {
142124 switch (state) {
143143- case 'pass':
125125+ case 'passed':
144126 return pass
145145- case 'fail':
127127+ case 'failed':
146128 return fail
147147- case 'skip':
148148- case 'todo':
129129+ case 'skipped':
149130 return skip
150131 default:
151132 return pending
···156137 * Format test states into string while keeping ANSI escapes at minimal.
157138 * Sibling icons with same color are merged into a single c.color() call.
158139 */
159159-function formatTests(states: TaskState[]): string {
140140+function formatTests(states: TestCaseState[]): string {
160141 let currentIcon = pending
161142 let count = 0
162143 let output = ''
+4-2
packages/vitest/src/node/reporters/index.ts
···11-import type { Reporter } from '../types/reporter'
11+import type { Reporter, TestRunEndReason } from '../types/reporter'
22import type { BaseOptions, BaseReporter } from './base'
33import type { BlobOptions } from './blob'
44import type { DefaultReporterOptions } from './default'
···2727 TapReporter,
2828 VerboseReporter,
2929}
3030-export type { BaseReporter, Reporter }
3030+export type { BaseReporter, Reporter, TestRunEndReason }
31313232export {
3333 BenchmarkBuiltinReporters,
···7070 'hanging-process': never
7171 'html': HTMLOptions
7272}
7373+7474+export type { ReportedHookContext } from './reported-tasks'
···55 Suite as RunnerTestSuite,
66 TaskMeta,
77} from '@vitest/runner'
88-import type { TestError } from '@vitest/utils'
88+import type { SerializedError, TestError } from '@vitest/utils'
99import type { TestProject } from '../project'
10101111class ReportedTaskImplementation {
···122122 }
123123124124 /**
125125- * Test results. Will be `undefined` if test is skipped, not finished yet or was just collected.
125125+ * Test results.
126126+ * - **pending**: Test was collected, but didn't finish running yet.
127127+ * - **passed**: Test passed successfully
128128+ * - **failed**: Test failed to execute
129129+ * - **skipped**: Test was skipped during collection or dynamically with `ctx.skip()`.
126130 */
127127- public result(): TestResult | undefined {
131131+ public result(): TestResult {
128132 const result = this.task.result
133133+ const mode = result?.state || this.task.mode
134134+135135+ if (!result && (mode === 'skip' || mode === 'todo')) {
136136+ return {
137137+ state: 'skipped',
138138+ note: undefined,
139139+ errors: undefined,
140140+ }
141141+ }
142142+129143 if (!result || result.state === 'run' || result.state === 'queued') {
130130- return undefined
144144+ return {
145145+ state: 'pending',
146146+ errors: undefined,
147147+ }
131148 }
132149 const state = result.state === 'fail'
133150 ? 'failed' as const
···154171 }
155172156173 /**
157157- * Checks if the test was skipped during collection or dynamically with `ctx.skip()`.
158158- */
159159- public skipped(): boolean {
160160- const mode = this.task.result?.state || this.task.mode
161161- return mode === 'skip' || mode === 'todo'
162162- }
163163-164164- /**
165174 * Custom metadata that was attached to the test during its execution.
166175 */
167176 public meta(): TaskMeta {
···175184 public diagnostic(): TestDiagnostic | undefined {
176185 const result = this.task.result
177186 // startTime should always be available if the test has properly finished
178178- if (!result || result.state === 'run' || result.state === 'queued' || !result.startTime) {
187187+ if (!result || !result.startTime) {
179188 return undefined
180189 }
181190 const duration = result.duration || 0
···228237 /**
229238 * Filters all tests that are part of this collection and its children.
230239 */
231231- *allTests(state?: TestResult['state'] | 'running'): Generator<TestCase, undefined, void> {
240240+ *allTests(state?: TestState): Generator<TestCase, undefined, void> {
232241 for (const child of this) {
233242 if (child.type === 'suite') {
234243 yield * child.children.allTests(state)
235244 }
236245 else if (state) {
237237- const testState = getTestState(child)
246246+ const testState = child.result().state
238247 if (state === testState) {
239248 yield child
240249 }
···248257 /**
249258 * Filters only the tests that are part of this collection.
250259 */
251251- *tests(state?: TestResult['state'] | 'running'): Generator<TestCase, undefined, void> {
260260+ *tests(state?: TestState): Generator<TestCase, undefined, void> {
252261 for (const child of this) {
253262 if (child.type !== 'test') {
254263 continue
255264 }
256265257266 if (state) {
258258- const testState = getTestState(child)
267267+ const testState = child.result().state
259268 if (state === testState) {
260269 yield child
261270 }
···298307299308export type { TestCollection }
300309310310+export type ReportedHookContext = {
311311+ readonly name: 'beforeAll' | 'afterAll'
312312+ readonly entity: TestSuite | TestModule
313313+} | {
314314+ readonly name: 'beforeEach' | 'afterEach'
315315+ readonly entity: TestCase
316316+}
317317+301318abstract class SuiteImplementation extends ReportedTaskImplementation {
302319 /** @internal */
303320 declare public readonly task: RunnerTestSuite | RunnerTestFile
···314331 }
315332316333 /**
317317- * Checks if the suite was skipped during collection.
318318- */
319319- public skipped(): boolean {
320320- const mode = this.task.mode
321321- return mode === 'skip' || mode === 'todo'
322322- }
323323-324324- /**
325334 * Errors that happened outside of the test run during collection, like syntax errors.
326335 */
327327- public errors(): TestError[] {
328328- return (this.task.result?.errors as TestError[] | undefined) || []
336336+ public errors(): SerializedError[] {
337337+ return (this.task.result?.errors as SerializedError[] | undefined) || []
329338 }
330339}
331340···379388 declare public ok: () => boolean
380389381390 /**
391391+ * Checks the running state of the suite.
392392+ */
393393+ public state(): TestSuiteState {
394394+ return getSuiteState(this.task)
395395+ }
396396+397397+ /**
382398 * Full name of the suite including all parent suites separated with `>`.
383399 */
384400 public get fullName(): string {
···402418403419 /**
404420 * This is usually an absolute UNIX file path.
405405- * It can be a virtual id if the file is not on the disk.
406406- * This value corresponds to Vite's `ModuleGraph` id.
421421+ * It can be a virtual ID if the file is not on the disk.
422422+ * This value corresponds to the ID in the Vite's module graph.
407423 */
408424 public readonly moduleId: string
409425···414430 }
415431416432 /**
433433+ * Checks the running state of the test file.
434434+ */
435435+ public state(): TestModuleState {
436436+ const state = this.task.result?.state
437437+ if (state === 'queued') {
438438+ return 'queued'
439439+ }
440440+ return getSuiteState(this.task)
441441+ }
442442+443443+ /**
417444 * Checks if the module has any failed tests.
418445 * This will also return `false` if module failed during collection.
419446 */
420447 declare public ok: () => boolean
421421-422422- /**
423423- * Checks if the module was skipped and didn't run.
424424- */
425425- declare public skipped: () => boolean
426448427449 /**
428450 * Useful information about the module like duration, memory usage, etc.
···445467}
446468447469export interface TaskOptions {
448448- each: boolean | undefined
449449- concurrent: boolean | undefined
450450- shuffle: boolean | undefined
451451- retry: number | undefined
452452- repeats: number | undefined
453453- mode: 'run' | 'only' | 'skip' | 'todo' | 'queued'
470470+ readonly each: boolean | undefined
471471+ readonly fails: boolean | undefined
472472+ readonly concurrent: boolean | undefined
473473+ readonly shuffle: boolean | undefined
474474+ readonly retry: number | undefined
475475+ readonly repeats: number | undefined
476476+ readonly mode: 'run' | 'only' | 'skip' | 'todo'
454477}
455478456479function buildOptions(
457457- task: RunnerTestCase | RunnerTestFile | RunnerTestSuite,
480480+ task: RunnerTestCase | RunnerTestSuite,
458481): TaskOptions {
459482 return {
460483 each: task.each,
484484+ fails: task.type === 'test' && task.fails,
461485 concurrent: task.concurrent,
462486 shuffle: task.shuffle,
463487 retry: task.retry,
464488 repeats: task.repeats,
465465- mode: task.mode,
489489+ // runner types are too broad, but the public API should be more strict
490490+ // the queued state exists only on Files and this method is called
491491+ // only for tests and suites
492492+ mode: task.mode as TaskOptions['mode'],
466493 }
467494}
468495469469-export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped
496496+export type TestSuiteState = 'skipped' | 'pending' | 'failed' | 'passed'
497497+export type TestModuleState = TestSuiteState | 'queued'
498498+export type TestState = TestResult['state']
499499+500500+export type TestResult =
501501+ | TestResultPassed
502502+ | TestResultFailed
503503+ | TestResultSkipped
504504+ | TestResultPending
505505+506506+export interface TestResultPending {
507507+ /**
508508+ * The test was collected, but didn't finish running yet.
509509+ */
510510+ readonly state: 'pending'
511511+ /**
512512+ * Pending tests have no errors.
513513+ */
514514+ readonly errors: undefined
515515+}
470516471517export interface TestResultPassed {
472518 /**
473519 * The test passed successfully.
474520 */
475475- state: 'passed'
521521+ readonly state: 'passed'
476522 /**
477523 * Errors that were thrown during the test execution.
478524 *
479525 * **Note**: If test was retried successfully, errors will still be reported.
480526 */
481481- errors: TestError[] | undefined
527527+ readonly errors: ReadonlyArray<TestError> | undefined
482528}
483529484530export interface TestResultFailed {
485531 /**
486532 * The test failed to execute.
487533 */
488488- state: 'failed'
534534+ readonly state: 'failed'
489535 /**
490536 * Errors that were thrown during the test execution.
491537 */
492492- errors: TestError[]
538538+ readonly errors: ReadonlyArray<TestError>
493539}
494540495541export interface TestResultSkipped {
···497543 * The test was skipped with `only` (on another test), `skip` or `todo` flag.
498544 * You can see which one was used in the `options.mode` option.
499545 */
500500- state: 'skipped'
546546+ readonly state: 'skipped'
501547 /**
502548 * Skipped tests have no errors.
503549 */
504504- errors: undefined
550550+ readonly errors: undefined
505551 /**
506552 * A custom note passed down to `ctx.skip(note)`.
507553 */
508508- note: string | undefined
554554+ readonly note: string | undefined
509555}
510556511557export interface TestDiagnostic {
512558 /**
513559 * If the duration of the test is above `slowTestThreshold`.
514560 */
515515- slow: boolean
561561+ readonly slow: boolean
516562 /**
517563 * The amount of memory used by the test in bytes.
518564 * This value is only available if the test was executed with `logHeapUsage` flag.
519565 */
520520- heap: number | undefined
566566+ readonly heap: number | undefined
521567 /**
522568 * The time it takes to execute the test in ms.
523569 */
524524- duration: number
570570+ readonly duration: number
525571 /**
526572 * The time in ms when the test started.
527573 */
528528- startTime: number
574574+ readonly startTime: number
529575 /**
530576 * The amount of times the test was retried.
531577 */
532532- retryCount: number
578578+ readonly retryCount: number
533579 /**
534580 * The amount of times the test was repeated as configured by `repeats` option.
535581 * This value can be lower if the test failed during the repeat and no `retry` is configured.
536582 */
537537- repeatCount: number
583583+ readonly repeatCount: number
538584 /**
539585 * If test passed on a second retry.
540586 */
541541- flaky: boolean
587587+ readonly flaky: boolean
542588}
543589544590export interface ModuleDiagnostic {
545591 /**
546592 * The time it takes to import and initiate an environment.
547593 */
548548- environmentSetupDuration: number
594594+ readonly environmentSetupDuration: number
549595 /**
550596 * The time it takes Vitest to setup test harness (runner, mocks, etc.).
551597 */
552552- prepareDuration: number
598598+ readonly prepareDuration: number
553599 /**
554600 * The time it takes to import the test module.
555601 * This includes importing everything in the module and executing suite callbacks.
556602 */
557557- collectDuration: number
603603+ readonly collectDuration: number
558604 /**
559605 * The time it takes to import the setup module.
560606 */
561561- setupDuration: number
607607+ readonly setupDuration: number
562608 /**
563609 * Accumulated duration of all tests and hooks in the module.
564610 */
565565- duration: number
566566-}
567567-568568-function getTestState(test: TestCase): TestResult['state'] | 'running' {
569569- if (test.skipped()) {
570570- return 'skipped'
571571- }
572572- const result = test.result()
573573- return result ? result.state : 'running'
611611+ readonly duration: number
574612}
575613576614function storeTask(
···592630 )
593631 }
594632 return reportedTask
633633+}
634634+635635+function getSuiteState(task: RunnerTestSuite | RunnerTestFile): TestSuiteState {
636636+ const mode = task.mode
637637+ const state = task.result?.state
638638+ if (mode === 'skip' || mode === 'todo' || state === 'skip' || state === 'todo') {
639639+ return 'skipped'
640640+ }
641641+ if (state == null || state === 'run' || state === 'only') {
642642+ return 'pending'
643643+ }
644644+ if (state === 'fail') {
645645+ return 'failed'
646646+ }
647647+ if (state === 'pass') {
648648+ return 'passed'
649649+ }
650650+ throw new Error(`Unknown suite state: ${state}`)
595651}
+97-127
packages/vitest/src/node/reporters/summary.ts
···11-import type { File, Test } from '@vitest/runner'
21import type { Vitest } from '../core'
32import type { Reporter } from '../types/reporter'
44-import type { TestModule } from './reported-tasks'
55-import type { HookOptions } from './task-parser'
66-import { getTests } from '@vitest/runner/utils'
33+import type { ReportedHookContext, TestCase, TestModule } from './reported-tasks'
74import c from 'tinyrainbow'
85import { F_POINTER, F_TREE_NODE_END, F_TREE_NODE_MIDDLE } from './renderers/figures'
96import { formatProjectName, formatTime, formatTimeString, padSummaryTitle } from './renderers/utils'
107import { WindowRenderer } from './renderers/windowedRenderer'
1111-import { TaskParser } from './task-parser'
128139const DURATION_UPDATE_INTERVAL_MS = 100
1410const FINISHED_TEST_CLEANUP_TIME_MS = 1_000
···3430 hook?: Omit<SlowTask, 'hook'>
3531}
36323737-interface RunningTest extends Pick<Counter, 'total' | 'completed'> {
3838- filename: File['name']
3939- projectName: File['projectName']
3333+interface RunningModule extends Pick<Counter, 'total' | 'completed'> {
3434+ filename: TestModule['task']['name']
3535+ projectName: TestModule['project']['name']
4036 hook?: Omit<SlowTask, 'hook'>
4141- tests: Map<Test['id'], SlowTask>
3737+ tests: Map<TestCase['id'], SlowTask>
3838+ typecheck: boolean
4239}
43404441/**
4542 * Reporter extension that renders summary and forwards all other logs above itself.
4643 * Intended to be used by other reporters, not as a standalone reporter.
4744 */
4848-export class SummaryReporter extends TaskParser implements Reporter {
4545+export class SummaryReporter implements Reporter {
4646+ private ctx!: Vitest
4947 private options!: Options
5048 private renderer!: WindowRenderer
51495252- private suites = emptyCounters()
5050+ private modules = emptyCounters()
5351 private tests = emptyCounters()
5452 private maxParallelTests = 0
55535656- /** Currently running tests, may include finished tests too */
5757- private runningTests = new Map<File['id'], RunningTest>()
5454+ /** Currently running test modules, may include finished test modules too */
5555+ private runningModules = new Map<TestModule['id'], RunningModule>()
58565959- /** ID of finished `this.runningTests` that are currently being shown */
6060- private finishedTests = new Map<File['id'], NodeJS.Timeout>()
6161-6262- /** IDs of all finished tests */
6363- private allFinishedTests = new Set<File['id']>()
5757+ /** ID of finished `this.runningModules` that are currently being shown */
5858+ private finishedModules = new Map<TestModule['id'], NodeJS.Timeout>()
64596560 private startTime = ''
6661 private currentTime = 0
···8883 })
8984 }
90859191- onTestModuleQueued(module: TestModule) {
9292- this.onTestFilePrepare(module.task)
9393- }
9494-9586 onPathsCollected(paths?: string[]) {
9696- this.suites.total = (paths || []).length
8787+ this.modules.total = (paths || []).length
9788 }
98899990 onWatcherRerun() {
100100- this.runningTests.clear()
101101- this.finishedTests.clear()
102102- this.allFinishedTests.clear()
103103- this.suites = emptyCounters()
9191+ this.runningModules.clear()
9292+ this.finishedModules.clear()
9393+ this.modules = emptyCounters()
10494 this.tests = emptyCounters()
1059510696 this.startTimers()
···10898 }
10999110100 onFinished() {
111111- this.runningTests.clear()
112112- this.finishedTests.clear()
113113- this.allFinishedTests.clear()
101101+ this.runningModules.clear()
102102+ this.finishedModules.clear()
114103 this.renderer.finish()
115104 clearInterval(this.durationInterval)
116105 }
117106118118- onTestFilePrepare(file: File) {
119119- if (this.runningTests.has(file.id)) {
120120- const stats = this.runningTests.get(file.id)!
121121- // if there are no tests, it means the test was queued but not collected
122122- if (!stats.total) {
123123- const total = getTests(file).length
124124- this.tests.total += total
125125- stats.total = total
126126- }
127127- return
107107+ onTestModuleQueued(module: TestModule) {
108108+ // When new test module starts, take the place of previously finished test module, if any
109109+ if (this.finishedModules.size) {
110110+ const finished = this.finishedModules.keys().next().value
111111+ this.removeTestModule(finished)
128112 }
129113130130- if (this.allFinishedTests.has(file.id)) {
131131- return
132132- }
133133-134134- const total = getTests(file).length
135135- this.tests.total += total
136136-137137- // When new test starts, take the place of previously finished test, if any
138138- if (this.finishedTests.size) {
139139- const finished = this.finishedTests.keys().next().value
140140- this.removeTestFile(finished)
141141- }
142142-143143- this.runningTests.set(file.id, {
144144- total,
145145- completed: 0,
146146- filename: file.name,
147147- projectName: file.projectName,
148148- tests: new Map(),
149149- })
150150-151151- this.maxParallelTests = Math.max(this.maxParallelTests, this.runningTests.size)
114114+ this.runningModules.set(module.id, initializeStats(module))
152115 }
153116154154- onHookStart(options: HookOptions) {
117117+ onTestModuleCollected(module: TestModule) {
118118+ let stats = this.runningModules.get(module.id)
119119+120120+ if (!stats) {
121121+ stats = initializeStats(module)
122122+ this.runningModules.set(module.id, stats)
123123+ }
124124+125125+ const total = Array.from(module.children.allTests()).length
126126+ this.tests.total += total
127127+ stats.total = total
128128+129129+ this.maxParallelTests = Math.max(this.maxParallelTests, this.runningModules.size)
130130+ }
131131+132132+ onHookStart(options: ReportedHookContext) {
155133 const stats = this.getHookStats(options)
156134157135 if (!stats) {
···174152 hook.onFinish = () => clearTimeout(timeout)
175153 }
176154177177- onHookEnd(options: HookOptions) {
155155+ onHookEnd(options: ReportedHookContext) {
178156 const stats = this.getHookStats(options)
179157180158 if (stats?.hook?.name !== options.name) {
···185163 stats.hook.visible = false
186164 }
187165188188- onTestStart(test: Test) {
166166+ onTestCaseReady(test: TestCase) {
189167 // Track slow running tests only on verbose mode
190168 if (!this.options.verbose) {
191169 return
192170 }
193171194194- const stats = this.getTestStats(test)
172172+ const stats = this.runningModules.get(test.module.id)
195173196174 if (!stats || stats.tests.has(test.id)) {
197175 return
···216194 stats.tests.set(test.id, slowTest)
217195 }
218196219219- onTestFinished(test: Test) {
220220- const stats = this.getTestStats(test)
197197+ onTestCaseResult(test: TestCase) {
198198+ const stats = this.runningModules.get(test.module.id)
221199222200 if (!stats) {
223201 return
···227205 stats.tests.delete(test.id)
228206229207 stats.completed++
230230- const result = test.result
208208+ const result = test.result()
231209232232- if (result?.state === 'pass') {
210210+ if (result?.state === 'passed') {
233211 this.tests.passed++
234212 }
235235- else if (result?.state === 'fail') {
213213+ else if (result?.state === 'failed') {
236214 this.tests.failed++
237215 }
238238- else if (!result?.state || result?.state === 'skip' || result?.state === 'todo') {
216216+ else if (!result?.state || result?.state === 'skipped') {
239217 this.tests.skipped++
240218 }
241219 }
242220243243- onTestFileFinished(file: File) {
244244- if (this.allFinishedTests.has(file.id)) {
245245- return
221221+ onTestModuleEnd(module: TestModule) {
222222+ const state = module.state()
223223+ this.modules.completed++
224224+225225+ if (state === 'passed') {
226226+ this.modules.passed++
227227+ }
228228+ else if (state === 'failed') {
229229+ this.modules.failed++
230230+ }
231231+ else if (module.task.mode === 'todo' && state === 'skipped') {
232232+ this.modules.todo++
233233+ }
234234+ else if (state === 'skipped') {
235235+ this.modules.skipped++
246236 }
247237248248- this.allFinishedTests.add(file.id)
249249- this.suites.completed++
250250-251251- if (file.result?.state === 'pass') {
252252- this.suites.passed++
253253- }
254254- else if (file.result?.state === 'fail') {
255255- this.suites.failed++
256256- }
257257- else if (file.result?.state === 'skip') {
258258- this.suites.skipped++
259259- }
260260- else if (file.result?.state === 'todo') {
261261- this.suites.todo++
262262- }
263263-264264- const left = this.suites.total - this.suites.completed
238238+ const left = this.modules.total - this.modules.completed
265239266240 // Keep finished tests visible in summary for a while if there are more tests left.
267267- // When a new test starts in onTestFilePrepare it will take this ones place.
241241+ // When a new test starts in onTestModuleQueued it will take this ones place.
268242 // This reduces flickering by making summary more stable.
269243 if (left > this.maxParallelTests) {
270270- this.finishedTests.set(file.id, setTimeout(() => {
271271- this.removeTestFile(file.id)
244244+ this.finishedModules.set(module.id, setTimeout(() => {
245245+ this.removeTestModule(module.id)
272246 }, FINISHED_TEST_CLEANUP_TIME_MS).unref())
273247 }
274248 else {
275249 // Run is about to end as there are less tests left than whole run had parallel at max.
276276- // Remove finished test immediately.
277277- this.removeTestFile(file.id)
250250+ // Remove finished test immediatelly.
251251+ this.removeTestModule(module.id)
278252 }
279253 }
280254281281- private getTestStats(test: Test) {
282282- const file = test.file
283283- let stats = this.runningTests.get(file.id)
284284-285285- if (!stats || stats.total === 0) {
286286- // It's possible that that test finished before it's preparation was even reported
287287- this.onTestFilePrepare(test.file)
288288- stats = this.runningTests.get(file.id)!
289289-290290- // It's also possible that this update came after whole test file was reported as finished
291291- if (!stats) {
292292- return
293293- }
294294- }
295295-296296- return stats
297297- }
298298-299299- private getHookStats({ file, id, type }: HookOptions) {
255255+ private getHookStats({ entity }: ReportedHookContext) {
300256 // Track slow running hooks only on verbose mode
301257 if (!this.options.verbose) {
302258 return
303259 }
304260305305- const stats = this.runningTests.get(file.id)
261261+ const module = entity.type === 'module' ? entity : entity.module
262262+ const stats = this.runningModules.get(module.id)
306263307264 if (!stats) {
308265 return
309266 }
310267311311- return type === 'suite' ? stats : stats?.tests.get(id)
268268+ return entity.type === 'test' ? stats.tests.get(entity.id) : stats
312269 }
313270314271 private createSummary() {
315272 const summary = ['']
316273317317- for (const testFile of Array.from(this.runningTests.values()).sort(sortRunningTests)) {
274274+ for (const testFile of Array.from(this.runningModules.values()).sort(sortRunningModules)) {
275275+ const typecheck = testFile.typecheck ? `${c.bgBlue(c.bold(' TS '))} ` : ''
318276 summary.push(
319277 c.bold(c.yellow(` ${F_POINTER} `))
320278 + formatProjectName(testFile.projectName)
279279+ + typecheck
321280 + testFile.filename
322281 + c.dim(!testFile.completed && !testFile.total
323282 ? ' [queued]'
···345304 }
346305 }
347306348348- if (this.runningTests.size > 0) {
307307+ if (this.runningModules.size > 0) {
349308 summary.push('')
350309 }
351310352352- summary.push(padSummaryTitle('Test Files') + getStateString(this.suites))
311311+ summary.push(padSummaryTitle('Test Files') + getStateString(this.modules))
353312 summary.push(padSummaryTitle('Tests') + getStateString(this.tests))
354313 summary.push(padSummaryTitle('Start at') + this.startTime)
355314 summary.push(padSummaryTitle('Duration') + formatTime(this.duration))
···369328 }, DURATION_UPDATE_INTERVAL_MS).unref()
370329 }
371330372372- private removeTestFile(id?: File['id']) {
331331+ private removeTestModule(id?: TestModule['id']) {
373332 if (!id) {
374333 return
375334 }
376335377377- const testFile = this.runningTests.get(id)
336336+ const testFile = this.runningModules.get(id)
378337 testFile?.hook?.onFinish()
379338 testFile?.tests?.forEach(test => test.onFinish())
380339381381- this.runningTests.delete(id)
340340+ this.runningModules.delete(id)
382341383383- clearTimeout(this.finishedTests.get(id))
384384- this.finishedTests.delete(id)
342342+ clearTimeout(this.finishedModules.get(id))
343343+ this.finishedModules.delete(id)
385344 }
386345}
387346···402361 )
403362}
404363405405-function sortRunningTests(a: RunningTest, b: RunningTest) {
364364+function sortRunningModules(a: RunningModule, b: RunningModule) {
406365 if ((a.projectName || '') > (b.projectName || '')) {
407366 return 1
408367 }
···412371 }
413372414373 return a.filename.localeCompare(b.filename)
374374+}
375375+376376+function initializeStats(module: TestModule): RunningModule {
377377+ return {
378378+ total: 0,
379379+ completed: 0,
380380+ filename: module.task.name,
381381+ projectName: module.project.name,
382382+ tests: new Map(),
383383+ typecheck: !!module.task.meta.typecheck,
384384+ }
415385}
···11import type { File, TaskResultPack } from '@vitest/runner'
22+import type { SerializedError } from '@vitest/utils'
23import type { SerializedTestSpecification } from '../../runtime/types/utils'
34import type { Awaitable, UserConsoleLog } from '../../types/general'
45import type { Vitest } from '../core'
55-import type { TestModule } from '../reporters/reported-tasks'
66+import type { ReportedHookContext, TestCase, TestModule, TestSuite } from '../reporters/reported-tasks'
77+import type { TestSpecification } from '../spec'
88+99+export type TestRunEndReason = 'passed' | 'interrupted' | 'failed'
610711export interface Reporter {
88- onInit?: (ctx: Vitest) => void
1212+ onInit?: (vitest: Vitest) => void
1313+ /**
1414+ * @deprecated use `onTestRunStart` instead
1515+ */
916 onPathsCollected?: (paths?: string[]) => Awaitable<void>
1717+ /**
1818+ * @deprecated use `onTestRunStart` instead
1919+ */
1020 onSpecsCollected?: (specs?: SerializedTestSpecification[]) => Awaitable<void>
1111- onTestModuleQueued?: (file: TestModule) => Awaitable<void>
1212- onCollected?: (files?: File[]) => Awaitable<void>
2121+ /**
2222+ * @deprecated use `onTestModuleCollected` instead
2323+ */
2424+ onCollected?: (files: File[]) => Awaitable<void>
2525+ /**
2626+ * @deprecated use `onTestRunEnd` instead
2727+ */
1328 onFinished?: (
1429 files: File[],
1530 errors: unknown[],
1631 coverage?: unknown
1732 ) => Awaitable<void>
3333+ /**
3434+ * @deprecated use `onTestModuleQueued`, `onTestModuleStart`, `onTestModuleEnd`, `onTestCaseReady`, `onTestCaseResult` instead
3535+ */
1836 onTaskUpdate?: (packs: TaskResultPack[]) => Awaitable<void>
1937 onTestRemoved?: (trigger?: string) => Awaitable<void>
2038 onWatcherStart?: (files?: File[], errors?: unknown[]) => Awaitable<void>
···2240 onServerRestart?: (reason?: string) => Awaitable<void>
2341 onUserConsoleLog?: (log: UserConsoleLog) => Awaitable<void>
2442 onProcessTimeout?: () => Awaitable<void>
4343+4444+ /**
4545+ * Called when the new test run starts.
4646+ */
4747+ onTestRunStart?: (specifications: ReadonlyArray<TestSpecification>) => Awaitable<void>
4848+ /**
4949+ * Called when the test run is finished.
5050+ */
5151+ onTestRunEnd?: (
5252+ testModules: ReadonlyArray<TestModule>,
5353+ unhandledErrors: ReadonlyArray<SerializedError>,
5454+ reason: TestRunEndReason
5555+ ) => Awaitable<void>
5656+5757+ /**
5858+ * Called when the module is enqueued for testing. The file itself is not loaded yet.
5959+ */
6060+ onTestModuleQueued?: (testModule: TestModule) => Awaitable<void>
6161+ /**
6262+ * Called when the test file is loaded and the module is ready to run tests.
6363+ */
6464+ onTestModuleCollected?: (testModule: TestModule) => Awaitable<void>
6565+ /**
6666+ * Called when starting to run tests of the test file
6767+ */
6868+ onTestModuleStart?: (testModule: TestModule) => Awaitable<void>
6969+ /**
7070+ * Called when all tests of the test file have finished running.
7171+ */
7272+ onTestModuleEnd?: (testModule: TestModule) => Awaitable<void>
7373+7474+ /**
7575+ * Called when test case is ready to run.
7676+ * Called before the `beforeEach` hooks for the test are run.
7777+ */
7878+ onTestCaseReady?: (testCase: TestCase) => Awaitable<void>
7979+ /**
8080+ * Called after the test and its hooks are finished running.
8181+ * The `result()` cannot be `pending`.
8282+ */
8383+ onTestCaseResult?: (testCase: TestCase) => Awaitable<void>
8484+8585+ /**
8686+ * Called when test suite is ready to run.
8787+ * Called before the `beforeAll` hooks for the test are run.
8888+ */
8989+ onTestSuiteReady?: (testSuite: TestSuite) => Awaitable<void>
9090+ /**
9191+ * Called after the test suite and its hooks are finished running.
9292+ * The `state` cannot be `pending`.
9393+ */
9494+ onTestSuiteResult?: (testSuite: TestSuite) => Awaitable<void>
9595+9696+ /**
9797+ * Called before the hook starts to run.
9898+ */
9999+ onHookStart?: (hook: ReportedHookContext) => Awaitable<void>
100100+ /**
101101+ * Called after the hook finished running.
102102+ */
103103+ onHookEnd?: (hook: ReportedHookContext) => Awaitable<void>
104104+105105+ onCoverage?: (coverage: unknown) => Awaitable<void>
25106}