Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/expect/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Test } from '@vitest/runner/types'
import type { Assertion } from './types'
import { processError } from '@vitest/utils/error'
import { noop } from '@vitest/utils/helpers'

export function createAssertionMessage(
Expand Down Expand Up @@ -86,10 +85,11 @@ export function recordAsyncExpect(
}

function handleTestError(test: Test, err: unknown) {
test.result ||= { state: 'fail' }
test.result.state = 'fail'
test.result.errors ||= []
test.result.errors.push(processError(err))
// test.result ||= { state: 'fail' }
// test.result.state = 'fail'
// test.result.errors ||= []
// test.result.errors.push(processError(err))
test.context.recordError(err)
}

/** wrap assertion function to support `expect.soft` and provide assertion name as `_name` */
Expand Down
46 changes: 46 additions & 0 deletions packages/runner/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
TestContext,
WriteableTestContext,
} from './types/tasks'
import { processError } from '@vitest/utils/error'
import { getSafeTimers } from '@vitest/utils/timers'
import { manageArtifactAttachment, recordArtifact, recordAsyncOperation } from './artifact'
import { PendingError } from './errors'
Expand Down Expand Up @@ -240,6 +241,44 @@ export function createTestContext(
)
}

function recordError(error: unknown) {
test.result ||= { state: 'fail' }
test.result.state = 'fail'
test.result.errors ||= []
test.result.errors.push(processError(error))
}
context.recordError = recordError

function recordErrorOnTimeout(createError: () => unknown) {
const addError = () => {
// as cause?
// const timeoutError = context.signal.reason as Error;
// if (!timeoutError.cause) {
// timeoutError.cause = createError()
// }

// or as stack?
const timeoutError = context.signal.reason as Error
if (timeoutError && timeoutError.stack) {
copyStackTrace(timeoutError, createError() as any)
}

// or as dedicated error?
// recordError(createError())
}
context.signal.addEventListener('abort', addError)
Comment on lines +252 to +269
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are the three variants:

  • as cause
 FAIL  test/repro.test.ts > repro
Error: Test timed out in 2000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
 ❯ test/repro.test.ts:3:1
      1| import { expect, test } from 'vitest'
      2|
      3| test('repro', { timeout: 2000 }, async () => {
       | ^
      4|   await expect.poll(async () => {
      5|     return 4321

Caused by: Error: expect.poll did not succeed in time.
 ❯ test/repro.test.ts:6:26
  • as stack
 FAIL  test/repro.test.ts > repro
Error: Test timed out in 2000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
 ❯ test/repro.test.ts:6:26
      4|   await expect.poll(async () => {
      5|     return 4321
      6|   }, { timeout: 10000 }).toBe(1234)
       |                          ^
      7| })
      8|
  • as a separate error
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  test/repro.test.ts > repro
Error: expect.poll did not succeed in time.
 ❯ test/repro.test.ts:6:26
      4|   await expect.poll(async () => {
      5|     return 4321
      6|   }, { timeout: 10000 }).toBe(1234)
       |                          ^
      7| })
      8|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯

 FAIL  test/repro.test.ts > repro
Error: Test timed out in 2000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
 ❯ test/repro.test.ts:3:1
      1| import { expect, test } from 'vitest'
      2|
      3| test('repro', { timeout: 2000 }, async () => {
       | ^
      4|   await expect.poll(async () => {
      5|     return 4321

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯

const deregister = () => {
context.signal.removeEventListener('abort', addError)
}
if (Symbol.dispose) {
Object.assign(deregister, {
[Symbol.dispose]: deregister,
})
}
return deregister as any
}
context.recordErrorOnTimeout = recordErrorOnTimeout

return runner.extendTaskContext?.(context) || context
}

Expand All @@ -257,3 +296,10 @@ function makeTimeoutError(isHook: boolean, timeout: number, stackTraceError?: Er
}
return error
}

function copyStackTrace(target: Error, source: Error) {
if (source.stack !== undefined) {
target.stack = source.stack.replace(source.message, target.message)
}
return target
}
3 changes: 3 additions & 0 deletions packages/runner/src/types/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,9 @@ export interface TestContext {
(message: string, type?: string, attachment?: TestAttachment): Promise<TestAnnotation>
(message: string, attachment?: TestAttachment): Promise<TestAnnotation>
}

readonly recordError: (error: unknown) => void
readonly recordErrorOnTimeout: (createError: () => unknown) => (() => void) & Disposable
}

export type OnTestFailedHandler = (context: TestContext) => Awaitable<void>
Expand Down
7 changes: 7 additions & 0 deletions packages/vitest/src/integrations/chai/poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
// extracting inline snapshot location to validate and update new snapshots.
return function __VITEST_POLL_CHAIN__(this: any, ...args: any[]) {
const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR')
const deregister = test.context.recordErrorOnTimeout(() => {
return copyStackTrace(
new Error('expect.poll did not succeed in time.'),
STACK_TRACE_ERROR,
)
})
const promise = async () => {
chai.util.flag(assertion, '_name', key)
chai.util.flag(assertion, 'error', STACK_TRACE_ERROR)
Expand Down Expand Up @@ -174,6 +180,7 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
}
}
finally {
deregister()
clearTimeout(timerId)
}
}
Expand Down
2 changes: 2 additions & 0 deletions test/test-utils/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export class Cli {
return resolve()
}

// TODO: can use recordErrorOnTimeout to ensure this error message is surfaced
// when test timeout kicked in first.
const timeout = setTimeout(() => {
error.message = `Timeout when waiting for error "${expected}".\nReceived:\nstdout: ${this.stdout}\nstderr: ${this.stderr}`
reject(error)
Expand Down
Loading