diff --git a/docs/api/index.md b/docs/api/index.md
index 46c204496e06..36817aea75b1 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -1010,6 +1010,10 @@ test('performs an organization query', async () => {
})
```
+::: tip
+This hook is always called in reverse order and is not affected by [`sequence.hooks`](/config/#sequence-hooks) option.
+:::
+
### onTestFailed
This hook is called only after the test has failed. It is called after `afterEach` hooks since they can influence the test result. It receives a `TaskResult` object with the current test result. This hook is useful for debugging.
diff --git a/docs/config/index.md b/docs/config/index.md
index 156db2bb69e2..a207e60958e9 100644
--- a/docs/config/index.md
+++ b/docs/config/index.md
@@ -1865,6 +1865,10 @@ Changes the order in which hooks are executed.
- `list` will order all hooks in the order they are defined
- `parallel` will run hooks in a single group in parallel (hooks in parent suites will still run before the current suite's hooks)
+::: tip
+This option doesn't affect [`onTestFinished`](/api/#ontestfinished). It is always called in reverse order.
+:::
+
#### sequence.setupFiles 0.29.3+ {#sequence-setupfiles}
- **Type**: `'list' | 'parallel'`
diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts
index b0321fe6c06b..c0d55c414b21 100644
--- a/packages/runner/src/run.ts
+++ b/packages/runner/src/run.ts
@@ -1,4 +1,5 @@
import limit from 'p-limit'
+import type { Awaitable } from '@vitest/utils'
import { getSafeTimers, shuffle } from '@vitest/utils'
import { processError } from '@vitest/utils/error'
import type { DiffOptions } from '@vitest/utils/diff'
@@ -33,6 +34,19 @@ function getSuiteHooks(suite: Suite, name: keyof SuiteHooks, sequence: SequenceH
return hooks
}
+async function callTaskHooks(task: Task, hooks: ((result: TaskResult) => Awaitable)[], sequence: SequenceHooks) {
+ if (sequence === 'stack')
+ hooks = hooks.slice().reverse()
+
+ if (sequence === 'parallel') {
+ await Promise.all(hooks.map(fn => fn(task.result!)))
+ }
+ else {
+ for (const fn of hooks)
+ await fn(task.result!)
+ }
+}
+
export async function callSuiteHook(
suite: Suite,
currentTask: Task,
@@ -211,7 +225,7 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) {
}
try {
- await Promise.all(test.onFinished?.map(fn => fn(test.result!)) || [])
+ await callTaskHooks(test, test.onFinished || [], 'stack')
}
catch (e) {
failTask(test.result, e, runner.config.diffOptions)
@@ -219,7 +233,7 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) {
if (test.result.state === 'fail') {
try {
- await Promise.all(test.onFailed?.map(fn => fn(test.result!)) || [])
+ await callTaskHooks(test, test.onFailed || [], runner.config.sequence.hooks)
}
catch (e) {
failTask(test.result, e, runner.config.diffOptions)
diff --git a/test/core/test/on-finished.test.ts b/test/core/test/on-finished.test.ts
index 5f4f050d7534..83d049cf971f 100644
--- a/test/core/test/on-finished.test.ts
+++ b/test/core/test/on-finished.test.ts
@@ -1,6 +1,7 @@
import { expect, it, onTestFinished } from 'vitest'
const collected: any[] = []
+const multiple: any[] = []
it('on-finished regular', () => {
collected.push(1)
@@ -38,6 +39,23 @@ it.fails('failed finish context', (t) => {
collected.push(null)
})
+it('multiple on-finished', () => {
+ onTestFinished(() => {
+ multiple.push(1)
+ })
+ onTestFinished(() => {
+ multiple.push(2)
+ })
+ onTestFinished(async () => {
+ await new Promise(r => setTimeout(r, 100))
+ multiple.push(3)
+ })
+ onTestFinished(() => {
+ multiple.push(4)
+ })
+})
+
it('after', () => {
expect(collected).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
+ expect(multiple).toEqual([4, 3, 2, 1])
})