From 7ccf8583263eedf9301fe3b89664b02790c1ec0a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 7 Nov 2024 13:46:26 +0100 Subject: [PATCH 01/16] fix: throw an error if `.poll` or `.element` wasn't awaited --- .../src/client/tester/expect-element.ts | 2 ++ packages/vitest/src/integrations/chai/poll.ts | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/client/tester/expect-element.ts b/packages/browser/src/client/tester/expect-element.ts index 58a2a5a6cffb..369756b019f4 100644 --- a/packages/browser/src/client/tester/expect-element.ts +++ b/packages/browser/src/client/tester/expect-element.ts @@ -14,6 +14,8 @@ export async function setupExpectDom() { if (elementOrLocator instanceof Element || elementOrLocator == null) { return elementOrLocator } + chai.util.flag(this, '_poll.element', true) + const isNot = chai.util.flag(this, 'negate') const name = chai.util.flag(this, '_name') // special case for `toBeInTheDocument` matcher diff --git a/packages/vitest/src/integrations/chai/poll.ts b/packages/vitest/src/integrations/chai/poll.ts index 1a7176021f60..bd5e3e3259f7 100644 --- a/packages/vitest/src/integrations/chai/poll.ts +++ b/packages/vitest/src/integrations/chai/poll.ts @@ -1,4 +1,5 @@ import type { Assertion, ExpectStatic } from '@vitest/expect' +import type { Test } from '@vitest/runner' import { getSafeTimers } from '@vitest/utils' import * as chai from 'chai' import { getWorkerState } from '../../runtime/utils' @@ -39,6 +40,10 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { poll: true, }) as Assertion fn = fn.bind(assertion) + const test = chai.util.flag(assertion, 'vitest-test') as Test | undefined + if (!test) { + throw new Error('expect.poll() must be called inside a test') + } const proxy: any = new Proxy(assertion, { get(target, key, receiver) { const assertionFunction = Reflect.get(target, key, receiver) @@ -59,7 +64,7 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { return function (this: any, ...args: any[]) { const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR') - return new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { let intervalId: any let lastError: any const { setTimeout, clearTimeout } = getSafeTimers() @@ -90,6 +95,21 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { } check() }) + let awaited = false + test.onFinished ??= [] + test.onFinished.push(() => { + if (!awaited) { + const name = chai.util.flag(assertion, '_poll.element') ? 'element(locator)' : 'poll(assertion)' + const error = new Error(`expect.${name}.${String(key)}() was not awaited. This assertion is asynchronous and must be awaited:\n\nawait expect.${name}.${String(key)}()\n`) + throw copyStackTrace(error, STACK_TRACE_ERROR) + } + }) + return { + then(onFulfilled, onRejected) { + awaited = true + return promise.then(onFulfilled, onRejected) + }, + } satisfies PromiseLike } }, }) From 959d362dedec467cfab9bb7f266e51bf66f30010 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 7 Nov 2024 13:53:03 +0100 Subject: [PATCH 02/16] chore: more promises --- packages/vitest/src/integrations/chai/poll.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/integrations/chai/poll.ts b/packages/vitest/src/integrations/chai/poll.ts index bd5e3e3259f7..3367d666f1bc 100644 --- a/packages/vitest/src/integrations/chai/poll.ts +++ b/packages/vitest/src/integrations/chai/poll.ts @@ -104,12 +104,21 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { throw copyStackTrace(error, STACK_TRACE_ERROR) } }) + // only .then is enough to check awaited, but we type this as `Promise` in global types + // so let's follow it return { then(onFulfilled, onRejected) { awaited = true return promise.then(onFulfilled, onRejected) }, - } satisfies PromiseLike + catch(onRejected) { + return promise.catch(onRejected) + }, + finally(onFinally) { + return promise.finally(onFinally) + }, + [Symbol.toStringTag]: 'Promise', + } satisfies Promise } }, }) From 0cbfaf4abee881f71be6c260a8993a13c710d73b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 7 Nov 2024 14:36:40 +0100 Subject: [PATCH 03/16] fix: run all onTestFinished/onTestFailed hooks even if one fails --- packages/runner/src/run.ts | 42 ++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index bb3c73105255..0d9cbeeb14b9 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -61,7 +61,8 @@ function getSuiteHooks( return hooks } -async function callTaskHooks( +async function callTestHooks( + runner: VitestRunner, task: Task, hooks: ((result: TaskResult) => Awaitable)[], sequence: SequenceHooks, @@ -71,11 +72,21 @@ async function callTaskHooks( } if (sequence === 'parallel') { - await Promise.all(hooks.map(fn => fn(task.result!))) + try { + await Promise.all(hooks.map(fn => fn(task.result!))) + } + catch (e) { + failTask(task.result!, e, runner.config.diffOptions) + } } else { for (const fn of hooks) { - await fn(task.result!) + try { + await fn(task.result!) + } + catch (e) { + failTask(task.result!, e, runner.config.diffOptions) + } } } } @@ -271,24 +282,15 @@ export async function runTest(test: Test | Custom, runner: VitestRunner): Promis failTask(test.result, e, runner.config.diffOptions) } - try { - await callTaskHooks(test, test.onFinished || [], 'stack') - } - catch (e) { - failTask(test.result, e, runner.config.diffOptions) - } + await callTestHooks(runner, test, test.onFinished || [], 'stack') if (test.result.state === 'fail') { - try { - await callTaskHooks( - test, - test.onFailed || [], - runner.config.sequence.hooks, - ) - } - catch (e) { - failTask(test.result, e, runner.config.diffOptions) - } + await callTestHooks( + runner, + test, + test.onFailed || [], + runner.config.sequence.hooks, + ) } delete test.onFailed @@ -331,7 +333,7 @@ export async function runTest(test: Test | Custom, runner: VitestRunner): Promis updateTask(test, runner) } -function failTask(result: TaskResult, err: unknown, diffOptions?: DiffOptions) { +function failTask(result: TaskResult, err: unknown, diffOptions: DiffOptions | undefined) { if (err instanceof PendingError) { result.state = 'skip' return From 4ae82aa67ad51375de6df98512762340b87fc472 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 7 Nov 2024 15:12:50 +0100 Subject: [PATCH 04/16] docs: mention the test will fail if poll is not awaited --- docs/api/expect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/expect.md b/docs/api/expect.md index 1103ee6a15cb..c6ea11a48c41 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -82,7 +82,7 @@ test('element exists', async () => { ``` ::: warning -`expect.poll` makes every assertion asynchronous, so do not forget to await it otherwise you might get unhandled promise rejections. +`expect.poll` makes every assertion asynchronous, so you need to await it. Since Vitest 2.2, if you forget to await it, the test will fail with a warning to do so. `expect.poll` doesn't work with several matchers: From b53d9ef4257fbfe9da53bf2a5268348ac9b5a08c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 7 Nov 2024 15:17:00 +0100 Subject: [PATCH 05/16] test: add failing tests --- .../fixtures/fails/poll-no-awaited.test.ts | 20 +++++++++++++++++++ .../cli/test/__snapshots__/fails.test.ts.snap | 9 +++++++++ 2 files changed, 29 insertions(+) create mode 100644 test/cli/fixtures/fails/poll-no-awaited.test.ts diff --git a/test/cli/fixtures/fails/poll-no-awaited.test.ts b/test/cli/fixtures/fails/poll-no-awaited.test.ts new file mode 100644 index 000000000000..a1cc4d39975b --- /dev/null +++ b/test/cli/fixtures/fails/poll-no-awaited.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest'; + +test('poll is not awaited once', () => { + expect.poll(() => 2).toBe(2) +}) + +test('poll is not awaited several times', () => { + expect.poll(() => 3).toBe(3) + expect.poll(() => 'string').toBe('string') +}) + +test('poll is not awaited but there is an async assertion afterwards', async () => { + expect.poll(() => 4).toBe(4) + await expect(new Promise((r) => setTimeout(() => r(3), 50))).resolves.toBe(3) +}) + +test('poll is not awaited but there is an error afterwards', async () => { + expect.poll(() => 4).toBe(4) + expect(3).toBe(4) +}) diff --git a/test/cli/test/__snapshots__/fails.test.ts.snap b/test/cli/test/__snapshots__/fails.test.ts.snap index 2b27922890ed..40960480b6d7 100644 --- a/test/cli/test/__snapshots__/fails.test.ts.snap +++ b/test/cli/test/__snapshots__/fails.test.ts.snap @@ -60,6 +60,15 @@ exports[`should fail no-assertions.test.ts > no-assertions.test.ts 1`] = `"Error exports[`should fail node-browser-context.test.ts > node-browser-context.test.ts 1`] = `"Error: @vitest/browser/context can be imported only inside the Browser Mode. Your test is running in forks pool. Make sure your regular tests are excluded from the "test.include" glob pattern."`; +exports[`should fail poll-no-awaited.test.ts > poll-no-awaited.test.ts 1`] = ` +"Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: +AssertionError: expected 3 to be 4 // Object.is equality +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited:" +`; + exports[`should fail primitive-error.test.ts > primitive-error.test.ts 1`] = `"Unknown Error: 42"`; exports[`should fail snapshot-with-not.test.ts > snapshot-with-not.test.ts 1`] = ` From f831179d64e4dad810454a65a668594a55224536 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 7 Nov 2024 15:27:43 +0100 Subject: [PATCH 06/16] fix: use `.not` in the error message if it was used --- packages/browser/src/client/tester/expect-element.ts | 4 ++-- packages/vitest/src/integrations/chai/poll.ts | 6 +++++- test/cli/fixtures/fails/poll-no-awaited.test.ts | 2 +- test/cli/test/__snapshots__/fails.test.ts.snap | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/browser/src/client/tester/expect-element.ts b/packages/browser/src/client/tester/expect-element.ts index 369756b019f4..7782853142d7 100644 --- a/packages/browser/src/client/tester/expect-element.ts +++ b/packages/browser/src/client/tester/expect-element.ts @@ -16,8 +16,8 @@ export async function setupExpectDom() { } chai.util.flag(this, '_poll.element', true) - const isNot = chai.util.flag(this, 'negate') - const name = chai.util.flag(this, '_name') + const isNot = chai.util.flag(this, 'negate') as boolean + const name = chai.util.flag(this, '_name') as string // special case for `toBeInTheDocument` matcher if (isNot && name === 'toBeInTheDocument') { return elementOrLocator.query() diff --git a/packages/vitest/src/integrations/chai/poll.ts b/packages/vitest/src/integrations/chai/poll.ts index 3367d666f1bc..237f4b607d12 100644 --- a/packages/vitest/src/integrations/chai/poll.ts +++ b/packages/vitest/src/integrations/chai/poll.ts @@ -99,8 +99,12 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { test.onFinished ??= [] test.onFinished.push(() => { if (!awaited) { + const negated = chai.util.flag(assertion, 'negate') ? 'not.' : '' const name = chai.util.flag(assertion, '_poll.element') ? 'element(locator)' : 'poll(assertion)' - const error = new Error(`expect.${name}.${String(key)}() was not awaited. This assertion is asynchronous and must be awaited:\n\nawait expect.${name}.${String(key)}()\n`) + const assertionString = `expect.${name}.${negated}${String(key)}()` + const error = new Error( + `${assertionString} was not awaited. This assertion is asynchronous and must be awaited:\n\nawait ${assertionString}\n`, + ) throw copyStackTrace(error, STACK_TRACE_ERROR) } }) diff --git a/test/cli/fixtures/fails/poll-no-awaited.test.ts b/test/cli/fixtures/fails/poll-no-awaited.test.ts index a1cc4d39975b..b6eeab4f8d3d 100644 --- a/test/cli/fixtures/fails/poll-no-awaited.test.ts +++ b/test/cli/fixtures/fails/poll-no-awaited.test.ts @@ -6,7 +6,7 @@ test('poll is not awaited once', () => { test('poll is not awaited several times', () => { expect.poll(() => 3).toBe(3) - expect.poll(() => 'string').toBe('string') + expect.poll(() => 'string').not.toBe('correct') }) test('poll is not awaited but there is an async assertion afterwards', async () => { diff --git a/test/cli/test/__snapshots__/fails.test.ts.snap b/test/cli/test/__snapshots__/fails.test.ts.snap index 40960480b6d7..952e7db3a2e8 100644 --- a/test/cli/test/__snapshots__/fails.test.ts.snap +++ b/test/cli/test/__snapshots__/fails.test.ts.snap @@ -65,7 +65,7 @@ exports[`should fail poll-no-awaited.test.ts > poll-no-awaited.test.ts 1`] = ` AssertionError: expected 3 to be 4 // Object.is equality Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: -Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: +Error: expect.poll(assertion).not.toBe() was not awaited. This assertion is asynchronous and must be awaited: Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited:" `; From 9c8118666bcf910fe1fec3b19b558171a02c44b3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 8 Nov 2024 13:25:03 +0100 Subject: [PATCH 07/16] feat: show a warning if .promise/.rejects was not awaited --- packages/expect/src/jest-expect.ts | 20 +++++- packages/expect/src/utils.ts | 57 ++++++++++++++++- .../vitest/src/integrations/snapshot/chai.ts | 19 ++++-- test/cli/test/fails.test.ts | 63 ++++++++++++++++++- test/test-utils/index.ts | 9 ++- 5 files changed, 155 insertions(+), 13 deletions(-) diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index c8cb304ce193..b508aa3fb18f 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -22,7 +22,7 @@ import { subsetEquality, typeEquality, } from './jest-utils' -import { recordAsyncExpect, wrapAssertion } from './utils' +import { createAssertionMessage, recordAsyncExpect, wrapAssertion } from './utils' // polyfill globals because expect can be used in node environment declare class Node { @@ -983,6 +983,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { } return (...args: any[]) => { + utils.flag(this, '_name', key) const promise = obj.then( (value: any) => { utils.flag(this, 'object', value) @@ -1004,7 +1005,13 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { }, ) - return recordAsyncExpect(test, promise) + return recordAsyncExpect( + test, + promise, + createAssertionMessage(utils, this, !!args.length), + error, + 5, + ) } }, }) @@ -1045,6 +1052,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { } return (...args: any[]) => { + utils.flag(this, '_name', key) const promise = wrapper.then( (value: any) => { const _error = new AssertionError( @@ -1069,7 +1077,13 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { }, ) - return recordAsyncExpect(test, promise) + return recordAsyncExpect( + test, + promise, + createAssertionMessage(utils, this, !!args.length), + error, + 5, + ) } }, }) diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index a043cd9cd9cb..9bd0ccda3126 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -2,14 +2,33 @@ import type { Test } from '@vitest/runner/types' import type { Assertion } from './types' import { processError } from '@vitest/utils/error' +export function createAssertionMessage( + util: Chai.ChaiUtils, + assertion: Assertion, + hasArgs: boolean, +) { + const not = util.flag(assertion, 'negate') ? 'not.' : '' + const name = `${util.flag(assertion, '_name')}(${hasArgs ? 'expected' : ''})` + const promiseName = util.flag(assertion, 'promise') + const promise = promiseName ? `.${promiseName}` : '' + return `expect(actual)${promise}.${not}${name}` +} + export function recordAsyncExpect( - test: any, - promise: Promise | PromiseLike, + _test: any, + promise: Promise, + assertion: string, + error: Error, + stackIndex: number, ) { + const test = _test as Test | undefined // record promise for test, that resolves before test ends if (test && promise instanceof Promise) { // if promise is explicitly awaited, remove it from the list promise = promise.finally(() => { + if (!test.promises) { + return + } const index = test.promises.indexOf(promise) if (index !== -1) { test.promises.splice(index, 1) @@ -21,6 +40,35 @@ export function recordAsyncExpect( test.promises = [] } test.promises.push(promise) + + let resolved = false + test.onFinished ??= [] + test.onFinished.push(() => { + if (!resolved) { + const stack = error.stack?.split(/\n\r?/) + const printStack = stack?.slice(stackIndex).join('\n') + console.warn([ + `Promise returned by \`${assertion}\` was not awaited. `, + 'Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. ', + 'Please remember to await the assertion.\n', + printStack, + ].join('')) + } + }) + + return { + then(onFullfilled, onRejected) { + resolved = true + return promise.then(onFullfilled, onRejected) + }, + catch(onRejected) { + return promise.catch(onRejected) + }, + finally(onFinally) { + return promise.finally(onFinally) + }, + [Symbol.toStringTag]: 'Promise', + } satisfies Promise } return promise @@ -32,7 +80,10 @@ export function wrapAssertion( fn: (this: Chai.AssertionStatic & Assertion, ...args: any[]) => void, ) { return function (this: Chai.AssertionStatic & Assertion, ...args: any[]) { - utils.flag(this, '_name', name) + // private + if (name !== 'withTest') { + utils.flag(this, '_name', name) + } if (!utils.flag(this, 'soft')) { return fn.apply(this, args) diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 3837d2b88d20..2f63336a2965 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -1,4 +1,4 @@ -import type { ChaiPlugin } from '@vitest/expect' +import type { Assertion, ChaiPlugin } from '@vitest/expect' import type { Test } from '@vitest/runner' import { equals, iterableEquality, subsetEquality } from '@vitest/expect' import { getNames } from '@vitest/runner/utils' @@ -7,7 +7,7 @@ import { SnapshotClient, stripSnapshotIndentation, } from '@vitest/snapshot' -import { recordAsyncExpect } from '../../../../expect/src/utils' +import { createAssertionMessage, recordAsyncExpect } from '../../../../expect/src/utils' let _client: SnapshotClient @@ -64,6 +64,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { properties?: object, message?: string, ) { + utils.flag(this, '_name', key) const isNot = utils.flag(this, 'negate') if (isNot) { throw new Error(`${key} cannot be used with "not"`) @@ -90,11 +91,13 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { utils.addMethod( chai.Assertion.prototype, 'toMatchFileSnapshot', - function (this: Record, file: string, message?: string) { + function (this: Assertion, file: string, message?: string) { + utils.flag(this, '_name', 'toMatchFileSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { throw new Error('toMatchFileSnapshot cannot be used with "not"') } + const error = new Error('resolves') const expected = utils.flag(this, 'object') const test = utils.flag(this, 'vitest-test') as Test const errorMessage = utils.flag(this, 'message') @@ -110,7 +113,13 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { ...getTestNames(test), }) - return recordAsyncExpect(test, promise) + return recordAsyncExpect( + test, + promise, + createAssertionMessage(utils, this, true), + error, + 3, + ) }, ) @@ -123,6 +132,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { inlineSnapshot?: string, message?: string, ) { + utils.flag(this, '_name', 'toMatchInlineSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { throw new Error('toMatchInlineSnapshot cannot be used with "not"') @@ -162,6 +172,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { chai.Assertion.prototype, 'toThrowErrorMatchingSnapshot', function (this: Record, message?: string) { + utils.flag(this, '_name', 'toThrowErrorMatchingSnapshot') const isNot = utils.flag(this, 'negate') if (isNot) { throw new Error( diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts index b9e7e3343bea..16f897cf9918 100644 --- a/test/cli/test/fails.test.ts +++ b/test/cli/test/fails.test.ts @@ -1,8 +1,10 @@ +import type { TestCase } from 'vitest/node' +import path from 'node:path' import { resolve } from 'pathe' + import { glob } from 'tinyglobby' import { expect, it } from 'vitest' - -import { runVitest } from '../../test-utils' +import { runInlineTests, runVitest, ts } from '../../test-utils' const root = resolve(__dirname, '../fixtures/fails') const files = await glob(['**/*.test.ts'], { cwd: root, dot: true, expandDirectories: false }) @@ -50,3 +52,60 @@ it('should not report coverage when "coverag.reportOnFailure" has default value expect(stdout).not.toMatch('Coverage report from istanbul') }) + +it('prints a warning if the assertion is not awaited', async () => { + const { stderr, results, ctx, root } = await runInlineTests({ + 'base.test.js': ts` + import { expect, test } from 'vitest'; + + test('single not awaited', () => { + expect(Promise.resolve(1)).resolves.toBe(1) + }) + + test('several not awaited', () => { + expect(Promise.resolve(1)).resolves.toBe(1) + expect(Promise.reject(1)).rejects.toBe(1) + }) + + test('not awaited and failed', () => { + expect(Promise.resolve(1)).resolves.toBe(1) + expect(1).toBe(2) + }) + + test('toMatchSnapshot not awaited', () => { + expect(1).toMatchFileSnapshot('./snapshot.txt') + }) + `, + }) + expect(results[0].tasks).toHaveLength(4) + const failedTest = ctx!.state.getReportedEntity(results[0].tasks[2]) as TestCase + expect(failedTest.result()).toEqual({ + state: 'failed', + errors: [ + expect.objectContaining({ + message: expect.stringContaining('expected 1 to be 2'), + }), + ], + }) + const warnings: string[] = [] + const lines = stderr.split('\n') + lines.forEach((line, index) => { + if (line.includes('Promise returned by')) { + warnings.push(lines.slice(index, index + 2).join('\n').replace(root + path.sep, '')) + } + }) + expect(warnings).toMatchInlineSnapshot(` + [ + "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at base.test.js:5:33", + "Promise returned by \`expect(actual).rejects.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at base.test.js:10:32", + "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at base.test.js:9:33", + "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at base.test.js:14:33", + "Promise returned by \`expect(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. + at base.test.js:19:17", + ] + `) +}) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index eebe9535e8fe..a765ddd4ba43 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -4,9 +4,10 @@ import type { WorkspaceProjectConfiguration } from 'vitest/config' import type { UserConfig, Vitest, VitestRunMode } from 'vitest/node' import { webcrypto as crypto } from 'node:crypto' import fs from 'node:fs' +import { resolve } from 'node:path' import { Readable, Writable } from 'node:stream' import { fileURLToPath } from 'node:url' -import { dirname, resolve } from 'pathe' +import { dirname } from 'pathe' import { x } from 'tinyexec' import { afterEach, onTestFinished, type WorkerGlobalState } from 'vitest' import { startVitest } from 'vitest/node' @@ -291,6 +292,12 @@ export async function runInlineTests( }) return { fs, + root, ...vitest, + get results() { + return vitest.ctx?.state.getFiles() || [] + }, } } + +export const ts = String.raw From dcd5ea982c9c32ab714ffb7af2264dec26c31bec Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 8 Nov 2024 13:28:46 +0100 Subject: [PATCH 08/16] chore: cleanup --- test/cli/test/fails.test.ts | 6 +++--- test/test-utils/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts index 16f897cf9918..bbe5c391bd96 100644 --- a/test/cli/test/fails.test.ts +++ b/test/cli/test/fails.test.ts @@ -54,7 +54,7 @@ it('should not report coverage when "coverag.reportOnFailure" has default value }) it('prints a warning if the assertion is not awaited', async () => { - const { stderr, results, ctx, root } = await runInlineTests({ + const { stderr, results, root } = await runInlineTests({ 'base.test.js': ts` import { expect, test } from 'vitest'; @@ -77,8 +77,8 @@ it('prints a warning if the assertion is not awaited', async () => { }) `, }) - expect(results[0].tasks).toHaveLength(4) - const failedTest = ctx!.state.getReportedEntity(results[0].tasks[2]) as TestCase + expect(results[0].children.size).toEqual(4) + const failedTest = results[0].children.at(2) as TestCase expect(failedTest.result()).toEqual({ state: 'failed', errors: [ diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index a765ddd4ba43..2c1a735e7ea8 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -1,7 +1,7 @@ import type { Options } from 'tinyexec' import type { UserConfig as ViteUserConfig } from 'vite' import type { WorkspaceProjectConfiguration } from 'vitest/config' -import type { UserConfig, Vitest, VitestRunMode } from 'vitest/node' +import type { TestModule, UserConfig, Vitest, VitestRunMode } from 'vitest/node' import { webcrypto as crypto } from 'node:crypto' import fs from 'node:fs' import { resolve } from 'node:path' @@ -295,7 +295,7 @@ export async function runInlineTests( root, ...vitest, get results() { - return vitest.ctx?.state.getFiles() || [] + return (vitest.ctx?.state.getFiles() || []).map(file => vitest.ctx?.state.getReportedEntity(file) as TestModule) }, } } From 7f349d28706a601471eba8f9c0f54e5955ce9e55 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 8 Nov 2024 14:29:38 +0100 Subject: [PATCH 09/16] fix: correct stack trace in the browser, throw if locator.* was not awaited --- packages/browser/src/client/tester/context.ts | 11 ++++--- .../src/client/tester/locators/index.ts | 6 ++-- .../src/client/tester/locators/preview.ts | 20 ++++++------ packages/browser/src/client/tester/logger.ts | 6 ++-- packages/browser/src/client/tester/runner.ts | 13 ++++++-- packages/browser/src/client/utils.ts | 32 +++++++++++++++++++ packages/expect/src/jest-expect.ts | 2 -- packages/expect/src/utils.ts | 7 ++-- packages/utils/src/source-map.ts | 10 ++++++ .../vitest/src/integrations/snapshot/chai.ts | 1 - packages/vitest/src/runtime/worker.ts | 4 +++ packages/vitest/src/types/worker.ts | 1 + test/browser/specs/runner.test.ts | 16 +++++++--- test/browser/test/failing.test.ts | 11 +++++++ test/cli/test/fails.test.ts | 24 ++++++++++++++ 15 files changed, 129 insertions(+), 35 deletions(-) diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index 5cf0e294f2d9..af3ddbc248dd 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -11,7 +11,7 @@ import type { UserEventTabOptions, UserEventTypeOptions, } from '../../../context' -import { convertElementToCssSelector, getBrowserState, getWorkerState } from '../utils' +import { convertElementToCssSelector, ensureAwaited, getBrowserState, getWorkerState } from '../utils' // this file should not import anything directly, only types and utils @@ -46,6 +46,7 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent } await triggerCommand('__vitest_cleanup', keyboard) keyboard.unreleased = [] + return ensureAwaited(Promise.resolve()) }, click(element: Element | Locator, options: UserEventClickOptions = {}) { return convertToLocator(element).click(processClickOptions(options)) @@ -100,12 +101,13 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent { ...options, unreleased: keyboard.unreleased }, ) keyboard.unreleased = unreleased + return ensureAwaited(Promise.resolve()) }, tab(options: UserEventTabOptions = {}) { if (typeof __tl_user_event__ !== 'undefined') { return __tl_user_event__.tab(options) } - return triggerCommand('__vitest_tab', options) + return ensureAwaited(triggerCommand('__vitest_tab', options)) }, async keyboard(text: string) { if (typeof __tl_user_event__ !== 'undefined') { @@ -117,6 +119,7 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent keyboard, ) keyboard.unreleased = unreleased + return ensureAwaited(Promise.resolve()) }, } } @@ -167,12 +170,12 @@ export const page: BrowserPage = { const name = options.path || `${taskName.replace(/[^a-z0-9]/gi, '-')}-${number}.png` - return triggerCommand('__vitest_screenshot', name, { + return ensureAwaited(triggerCommand('__vitest_screenshot', name, { ...options, element: options.element ? convertToSelector(options.element) : undefined, - }) + })) }, getByRole() { throw new Error('Method "getByRole" is not implemented in the current provider.') diff --git a/packages/browser/src/client/tester/locators/index.ts b/packages/browser/src/client/tester/locators/index.ts index dd3cdea61ff1..8121c9b38b0d 100644 --- a/packages/browser/src/client/tester/locators/index.ts +++ b/packages/browser/src/client/tester/locators/index.ts @@ -22,7 +22,7 @@ import { Ivya, type ParsedSelector, } from 'ivya' -import { getBrowserState, getWorkerState } from '../../utils' +import { ensureAwaited, getBrowserState, getWorkerState } from '../../utils' import { getElementError } from '../public-utils' // we prefer using playwright locators because they are more powerful and support Shadow DOM @@ -202,11 +202,11 @@ export abstract class Locator { || this.worker.current?.file?.filepath || undefined - return this.rpc.triggerCommand( + return ensureAwaited(this.rpc.triggerCommand( this.state.contextId, command, filepath, args, - ) + )) } } diff --git a/packages/browser/src/client/tester/locators/preview.ts b/packages/browser/src/client/tester/locators/preview.ts index beb03c178b7f..74a367d15b75 100644 --- a/packages/browser/src/client/tester/locators/preview.ts +++ b/packages/browser/src/client/tester/locators/preview.ts @@ -9,7 +9,7 @@ import { getByTextSelector, getByTitleSelector, } from 'ivya' -import { convertElementToCssSelector } from '../../utils' +import { convertElementToCssSelector, ensureAwaited } from '../../utils' import { getElementError } from '../public-utils' import { Locator, selectorEngine } from './index' @@ -58,28 +58,28 @@ class PreviewLocator extends Locator { } click(): Promise { - return userEvent.click(this.element()) + return ensureAwaited(userEvent.click(this.element())) } dblClick(): Promise { - return userEvent.dblClick(this.element()) + return ensureAwaited(userEvent.dblClick(this.element())) } tripleClick(): Promise { - return userEvent.tripleClick(this.element()) + return ensureAwaited(userEvent.tripleClick(this.element())) } hover(): Promise { - return userEvent.hover(this.element()) + return ensureAwaited(userEvent.hover(this.element())) } unhover(): Promise { - return userEvent.unhover(this.element()) + return ensureAwaited(userEvent.unhover(this.element())) } async fill(text: string): Promise { await this.clear() - return userEvent.type(this.element(), text) + return ensureAwaited(userEvent.type(this.element(), text)) } async upload(file: string | string[] | File | File[]): Promise { @@ -100,7 +100,7 @@ class PreviewLocator extends Locator { return fileInstance }) const uploadFiles = await Promise.all(uploadPromise) - return userEvent.upload(this.element() as HTMLElement, uploadFiles) + return ensureAwaited(userEvent.upload(this.element() as HTMLElement, uploadFiles)) } selectOptions(options_: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise { @@ -110,7 +110,7 @@ class PreviewLocator extends Locator { } return option }) - return userEvent.selectOptions(this.element(), options as string[] | HTMLElement[]) + return ensureAwaited(userEvent.selectOptions(this.element(), options as string[] | HTMLElement[])) } async dropTo(): Promise { @@ -118,7 +118,7 @@ class PreviewLocator extends Locator { } clear(): Promise { - return userEvent.clear(this.element()) + return ensureAwaited(userEvent.clear(this.element())) } async screenshot(): Promise { diff --git a/packages/browser/src/client/tester/logger.ts b/packages/browser/src/client/tester/logger.ts index 30a5149cfd1d..e2e5219cc204 100644 --- a/packages/browser/src/client/tester/logger.ts +++ b/packages/browser/src/client/tester/logger.ts @@ -41,10 +41,8 @@ export function setupConsoleLogSpy() { trace(...args) const content = processLog(args) const error = new Error('$$Trace') - const stack = (error.stack || '') - .split('\n') - .slice(error.stack?.includes('$$Trace') ? 2 : 1) - .join('\n') + const processor = (globalThis as any).__vitest_worker__?.onFilterStackTrace || ((s: string) => s || '') + const stack = processor(error.stack || '') sendLog('stderr', `${content}\n${stack}`, true) } diff --git a/packages/browser/src/client/tester/runner.ts b/packages/browser/src/client/tester/runner.ts index bf1505ced25c..40ff4501b2c5 100644 --- a/packages/browser/src/client/tester/runner.ts +++ b/packages/browser/src/client/tester/runner.ts @@ -7,7 +7,8 @@ import { page, userEvent } from '@vitest/browser/context' import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } from 'vitest/browser' import { NodeBenchmarkRunner, VitestTestRunner } from 'vitest/runners' import { originalPositionFor, TraceMap } from 'vitest/utils' -import { executor } from '../utils' +import { createStackString, parseStacktrace } from '../../../../utils/src/source-map' +import { executor, getWorkerState } from '../utils' import { rpc } from './rpc' import { VitestBrowserSnapshotEnvironment } from './snapshot' @@ -29,7 +30,7 @@ export function createBrowserRunner( mocker: VitestBrowserClientMocker, state: WorkerGlobalState, coverageModule: CoverageHandler | null, -): { new (options: BrowserRunnerOptions): VitestRunner } { +): { new (options: BrowserRunnerOptions): VitestRunner & { sourceMapCache: Map } } { return class BrowserTestRunner extends runnerClass implements VitestRunner { public config: SerializedConfig hashMap = browserHashMap @@ -171,6 +172,14 @@ export async function initiateRunner( ]) runner.config.diffOptions = diffOptions cachedRunner = runner + getWorkerState().onFilterStackTrace = (stack: string) => { + const stacks = parseStacktrace(stack, { + getSourceMap(file) { + return runner.sourceMapCache.get(file) + }, + }) + return createStackString(stacks) + } return runner } diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index 168704878cb3..1856c0f9597b 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -25,6 +25,38 @@ export function getConfig(): SerializedConfig { return getBrowserState().config } +export function ensureAwaited(promise: Promise): Promise { + const test = getWorkerState().current + if (!test || test.type !== 'test') { + return promise + } + let awaited = false + const sourceError = new Error('STACK_TRACE_ERROR') + test.onFinished ??= [] + test.onFinished.push(() => { + if (!awaited) { + const error = new Error( + `The call was not awaited. This method is asynchronous and must be awaited.`, + ) + error.stack = sourceError.stack?.replace(sourceError.message, error.message) + throw error + } + }) + return { + then(onFulfilled, onRejected) { + awaited = true + return promise.then(onFulfilled, onRejected) + }, + catch(onRejected) { + return promise.catch(onRejected) + }, + finally(onFinally) { + return promise.finally(onFinally) + }, + [Symbol.toStringTag]: 'Promise', + } satisfies Promise +} + export interface BrowserRunnerState { files: string[] runningFiles: string[] diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index b508aa3fb18f..72615f364de1 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -1010,7 +1010,6 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { promise, createAssertionMessage(utils, this, !!args.length), error, - 5, ) } }, @@ -1082,7 +1081,6 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { promise, createAssertionMessage(utils, this, !!args.length), error, - 5, ) } }, diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 9bd0ccda3126..9d5c44be9173 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -19,7 +19,6 @@ export function recordAsyncExpect( promise: Promise, assertion: string, error: Error, - stackIndex: number, ) { const test = _test as Test | undefined // record promise for test, that resolves before test ends @@ -45,13 +44,13 @@ export function recordAsyncExpect( test.onFinished ??= [] test.onFinished.push(() => { if (!resolved) { - const stack = error.stack?.split(/\n\r?/) - const printStack = stack?.slice(stackIndex).join('\n') + const processor = (globalThis as any).__vitest_worker__?.onFilterStackTrace || ((s: string) => s || '') + const stack = processor(error.stack) console.warn([ `Promise returned by \`${assertion}\` was not awaited. `, 'Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. ', 'Please remember to await the assertion.\n', - printStack, + stack, ].join('')) } }) diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index 21b20a1605a8..0ca0382536ce 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -179,6 +179,16 @@ export function parseSingleV8Stack(raw: string): ParsedStack | null { } } +export function createStackString(stacks: ParsedStack[]): string { + return stacks.map((stack) => { + const line = `${stack.file}:${stack.line}:${stack.column}` + if (stack.method) { + return ` at ${stack.method}(${line})` + } + return ` at ${line}` + }).join('\n') +} + export function parseStacktrace( stack: string, options: StackTraceParserOptions = {}, diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 2f63336a2965..f0eff3d94adc 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -118,7 +118,6 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { promise, createAssertionMessage(utils, this, true), error, - 3, ) }, ) diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 00897b4f0a53..4ccfa155e408 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -1,6 +1,7 @@ import type { ContextRPC, WorkerGlobalState } from '../types/worker' import type { VitestWorker } from './workers/types' import { pathToFileURL } from 'node:url' +import { createStackString, parseStacktrace } from '@vitest/utils/source-map' import { workerId as poolId } from 'tinypool' import { ModuleCacheMap } from 'vite-node/client' import { loadEnvironment } from '../integrations/env/loader' @@ -90,6 +91,9 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) { }, rpc, providedContext: ctx.providedContext, + onFilterStackTrace(stack) { + return createStackString(parseStacktrace(stack)) + }, } satisfies WorkerGlobalState const methodName = method === 'collect' ? 'collectTests' : 'runTests' diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index b6dbf9c18032..2c8b9a0fc0c5 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -47,4 +47,5 @@ export interface WorkerGlobalState { environment: number prepare: number } + onFilterStackTrace?: (trace: string) => string } diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 15820327d6b2..726d3ca75403 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -101,8 +101,8 @@ log with a stack error with a stack ❯ test/logs.test.ts:59:10 `.trim()) - // console.trace doens't add additional stack trace - expect(stderr).not.toMatch('test/logs.test.ts:60:10') + // console.trace processes the stack trace correctly + expect(stderr).toMatch('test/logs.test.ts:60:10') }) test.runIf(browser === 'webkit')(`logs have stack traces in safari`, () => { @@ -115,16 +115,22 @@ log with a stack error with a stack ❯ test/logs.test.ts:59:16 `.trim()) - // console.trace doens't add additional stack trace - expect(stderr).not.toMatch('test/logs.test.ts:60:16') + // console.trace processes the stack trace correctly + expect(stderr).toMatch('test/logs.test.ts:60:16') }) test(`stack trace points to correct file in every browser`, () => { // dependeing on the browser it references either `.toBe()` or `expect()` - expect(stderr).toMatch(/test\/failing.test.ts:5:(12|17)/) + expect(stderr).toMatch(/test\/failing.test.ts:6:(12|17)/) // column is 18 in safari, 8 in others expect(stderr).toMatch(/throwError src\/error.ts:8:(18|8)/) + + expect(stderr).toContain('The call was not awaited. This method is asynchronous and must be awaited.') + expect(stderr).toMatch(/test\/failing.test.ts:14:(27|33)/) + expect(stderr).toMatch(/test\/failing.test.ts:18:(27|36)/) + expect(stderr).toMatch(/test\/failing.test.ts:19:(27|41)/) + expect(stderr).toMatch(/test\/failing.test.ts:20:(27|32)/) }) test('popup apis should log a warning', () => { diff --git a/test/browser/test/failing.test.ts b/test/browser/test/failing.test.ts index 14cb207fe897..fd520e5afc2e 100644 --- a/test/browser/test/failing.test.ts +++ b/test/browser/test/failing.test.ts @@ -1,3 +1,4 @@ +import { page } from '@vitest/browser/context' import { expect, it } from 'vitest' import { throwError } from '../src/error' @@ -8,3 +9,13 @@ it('correctly fails and prints a diff', () => { it('correctly print error in another file', () => { throwError() }) + +it('locator method is not awaited', () => { + page.getByRole('button').click() +}) + +it('several locator methods are not awaited', async () => { + page.getByRole('button').dblClick() + page.getByRole('button').selectOptions([]) + page.getByRole('button').fill('123') +}) diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts index bbe5c391bd96..caf7e175310a 100644 --- a/test/cli/test/fails.test.ts +++ b/test/cli/test/fails.test.ts @@ -109,3 +109,27 @@ it('prints a warning if the assertion is not awaited', async () => { ] `) }) + +it('prints a warning if the assertion is not awaited in the browser mode', async () => { + const { stderr } = await runInlineTests({ + './vitest.config.js': { + test: { + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + headless: true, + }, + }, + }, + 'base.test.js': ts` + import { expect, test } from 'vitest'; + + test('single not awaited', () => { + expect(Promise.resolve(1)).resolves.toBe(1) + }) + `, + }) + expect(stderr).toContain('Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited') + expect(stderr).toContain('base.test.js:5:33') +}) From bb508f4e8ba02375c0e5bff09aac0b8376d9f71f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 8 Nov 2024 14:39:32 +0100 Subject: [PATCH 10/16] docs: add docs about warnings and errors if async is not awaited --- docs/api/expect.md | 4 ++++ docs/guide/browser/locators.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/docs/api/expect.md b/docs/api/expect.md index c6ea11a48c41..0e12270e177b 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -1185,6 +1185,8 @@ test('buyApples returns new stock id', async () => { :::warning If the assertion is not awaited, then you will have a false-positive test that will pass every time. To make sure that assertions are actually called, you may use [`expect.assertions(number)`](#expect-assertions). + +Since Vitest 2.2, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 3, the test will be marked as "failed" if the assertion is not awaited. ::: ## rejects @@ -1214,6 +1216,8 @@ test('buyApples throws an error when no id provided', async () => { :::warning If the assertion is not awaited, then you will have a false-positive test that will pass every time. To make sure that assertions were actually called, you can use [`expect.assertions(number)`](#expect-assertions). + +Since Vitest 2.2, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 3, the test will be marked as "failed" if the assertion is not awaited. ::: ## expect.assertions diff --git a/docs/guide/browser/locators.md b/docs/guide/browser/locators.md index 9c6e50f8d191..eaf3a59be5b5 100644 --- a/docs/guide/browser/locators.md +++ b/docs/guide/browser/locators.md @@ -389,6 +389,8 @@ It is recommended to use this only after the other locators don't work for your ## Methods +All methods are asynchronous and must be awaited. Since Vitest 2.2, tests will fail if a method is not awaited. + ### click ```ts From f213d5c8a8ac9da22df9c6b7a2be0b178d789b2e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 8 Nov 2024 14:41:44 +0100 Subject: [PATCH 11/16] test: add a button --- test/browser/test/failing.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/browser/test/failing.test.ts b/test/browser/test/failing.test.ts index fd520e5afc2e..18cae57cc24b 100644 --- a/test/browser/test/failing.test.ts +++ b/test/browser/test/failing.test.ts @@ -2,6 +2,10 @@ import { page } from '@vitest/browser/context' import { expect, it } from 'vitest' import { throwError } from '../src/error' +document.body.innerHTML = ` + +` + it('correctly fails and prints a diff', () => { expect(1).toBe(2) }) From dd068fd525bf1e56ec67c1fa896c227f6df3eb1b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 8 Nov 2024 14:51:52 +0100 Subject: [PATCH 12/16] test: fix line numbers --- test/browser/specs/runner.test.ts | 10 +++++----- test/browser/test/failing.test.ts | 4 ++-- test/browser/test/userEvent.test.ts | 9 +++++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 726d3ca75403..ea49e1623ac1 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -121,16 +121,16 @@ error with a stack test(`stack trace points to correct file in every browser`, () => { // dependeing on the browser it references either `.toBe()` or `expect()` - expect(stderr).toMatch(/test\/failing.test.ts:6:(12|17)/) + expect(stderr).toMatch(/test\/failing.test.ts:10:(12|17)/) // column is 18 in safari, 8 in others expect(stderr).toMatch(/throwError src\/error.ts:8:(18|8)/) expect(stderr).toContain('The call was not awaited. This method is asynchronous and must be awaited.') - expect(stderr).toMatch(/test\/failing.test.ts:14:(27|33)/) - expect(stderr).toMatch(/test\/failing.test.ts:18:(27|36)/) - expect(stderr).toMatch(/test\/failing.test.ts:19:(27|41)/) - expect(stderr).toMatch(/test\/failing.test.ts:20:(27|32)/) + expect(stderr).toMatch(/test\/failing.test.ts:18:(27|33)/) + expect(stderr).toMatch(/test\/failing.test.ts:22:(27|36)/) + expect(stderr).toMatch(/test\/failing.test.ts:23:(27|39)/) + expect(stderr).toMatch(/test\/failing.test.ts:24:(27|33)/) }) test('popup apis should log a warning', () => { diff --git a/test/browser/test/failing.test.ts b/test/browser/test/failing.test.ts index 18cae57cc24b..dc53772533ef 100644 --- a/test/browser/test/failing.test.ts +++ b/test/browser/test/failing.test.ts @@ -20,6 +20,6 @@ it('locator method is not awaited', () => { it('several locator methods are not awaited', async () => { page.getByRole('button').dblClick() - page.getByRole('button').selectOptions([]) - page.getByRole('button').fill('123') + page.getByRole('button').tripleClick() + page.getByRole('button').click() }) diff --git a/test/browser/test/userEvent.test.ts b/test/browser/test/userEvent.test.ts index 10900dd1a3cf..2b81643bfcf8 100644 --- a/test/browser/test/userEvent.test.ts +++ b/test/browser/test/userEvent.test.ts @@ -11,9 +11,14 @@ const userEvent = _uE.setup() describe('userEvent.click', () => { test('correctly clicks a button', async () => { + const wrapper = document.createElement('div') + wrapper.style.height = '100px' + wrapper.style.width = '200px' + wrapper.style.backgroundColor = 'red' + wrapper.style.display = 'flex' + wrapper.style.justifyContent = 'center' + wrapper.style.alignItems = 'center' const button = document.createElement('button') - button.style.height = '100px' - button.style.width = '200px' button.textContent = 'Click me' document.body.appendChild(button) const onClick = vi.fn() From cc14a69b38ad5ebd397ff0110f492c2fe1eeda61 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 8 Nov 2024 14:53:14 +0100 Subject: [PATCH 13/16] chore: use resolve from pathe --- test/cli/test/fails.test.ts | 13 ++++++------- test/test-utils/index.ts | 3 +-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts index caf7e175310a..037fc6c3c86f 100644 --- a/test/cli/test/fails.test.ts +++ b/test/cli/test/fails.test.ts @@ -1,5 +1,4 @@ import type { TestCase } from 'vitest/node' -import path from 'node:path' import { resolve } from 'pathe' import { glob } from 'tinyglobby' @@ -91,21 +90,21 @@ it('prints a warning if the assertion is not awaited', async () => { const lines = stderr.split('\n') lines.forEach((line, index) => { if (line.includes('Promise returned by')) { - warnings.push(lines.slice(index, index + 2).join('\n').replace(root + path.sep, '')) + warnings.push(lines.slice(index, index + 2).join('\n').replace(`${root}/`, '/')) } }) expect(warnings).toMatchInlineSnapshot(` [ "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. - at base.test.js:5:33", + at /base.test.js:5:33", "Promise returned by \`expect(actual).rejects.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. - at base.test.js:10:32", + at /base.test.js:10:32", "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. - at base.test.js:9:33", + at /base.test.js:9:33", "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. - at base.test.js:14:33", + at /base.test.js:14:33", "Promise returned by \`expect(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion. - at base.test.js:19:17", + at /base.test.js:19:17", ] `) }) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index 2c1a735e7ea8..d63bb4429b12 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -4,10 +4,9 @@ import type { WorkspaceProjectConfiguration } from 'vitest/config' import type { TestModule, UserConfig, Vitest, VitestRunMode } from 'vitest/node' import { webcrypto as crypto } from 'node:crypto' import fs from 'node:fs' -import { resolve } from 'node:path' import { Readable, Writable } from 'node:stream' import { fileURLToPath } from 'node:url' -import { dirname } from 'pathe' +import { dirname, resolve } from 'pathe' import { x } from 'tinyexec' import { afterEach, onTestFinished, type WorkerGlobalState } from 'vitest' import { startVitest } from 'vitest/node' From 9c12714de0ce8eec77b6f9ed4fdccbb844f5aeba Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 13 Nov 2024 12:25:01 +0100 Subject: [PATCH 14/16] fix: don't call a promise if it wasn't awaited --- packages/browser/src/client/tester/context.ts | 79 ++++++++++--------- .../src/client/tester/locators/index.ts | 2 +- .../src/client/tester/locators/preview.ts | 18 ++--- packages/browser/src/client/utils.ts | 12 +-- packages/vitest/src/integrations/chai/poll.ts | 11 +-- test/browser/specs/runner.test.ts | 7 +- test/browser/test/failing.test.ts | 8 +- 7 files changed, 70 insertions(+), 67 deletions(-) diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index af3ddbc248dd..d648f50aa7ea 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -40,13 +40,14 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent return createUserEvent(__tl_user_event_base__, options) }, async cleanup() { - if (typeof __tl_user_event_base__ !== 'undefined') { - __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {}) - return - } - await triggerCommand('__vitest_cleanup', keyboard) - keyboard.unreleased = [] - return ensureAwaited(Promise.resolve()) + return ensureAwaited(async () => { + if (typeof __tl_user_event_base__ !== 'undefined') { + __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {}) + return + } + await triggerCommand('__vitest_cleanup', keyboard) + keyboard.unreleased = [] + }) }, click(element: Element | Locator, options: UserEventClickOptions = {}) { return convertToLocator(element).click(processClickOptions(options)) @@ -85,41 +86,45 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent // testing-library user-event async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.type( - element instanceof Element ? element : element.element(), + return ensureAwaited(async () => { + if (typeof __tl_user_event__ !== 'undefined') { + return __tl_user_event__.type( + element instanceof Element ? element : element.element(), + text, + options, + ) + } + + const selector = convertToSelector(element) + const { unreleased } = await triggerCommand<{ unreleased: string[] }>( + '__vitest_type', + selector, text, - options, + { ...options, unreleased: keyboard.unreleased }, ) - } - - const selector = convertToSelector(element) - const { unreleased } = await triggerCommand<{ unreleased: string[] }>( - '__vitest_type', - selector, - text, - { ...options, unreleased: keyboard.unreleased }, - ) - keyboard.unreleased = unreleased - return ensureAwaited(Promise.resolve()) + keyboard.unreleased = unreleased + }) }, tab(options: UserEventTabOptions = {}) { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.tab(options) - } - return ensureAwaited(triggerCommand('__vitest_tab', options)) + return ensureAwaited(() => { + if (typeof __tl_user_event__ !== 'undefined') { + return __tl_user_event__.tab(options) + } + return triggerCommand('__vitest_tab', options) + }) }, async keyboard(text: string) { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.keyboard(text) - } - const { unreleased } = await triggerCommand<{ unreleased: string[] }>( - '__vitest_keyboard', - text, - keyboard, - ) - keyboard.unreleased = unreleased - return ensureAwaited(Promise.resolve()) + return ensureAwaited(async () => { + if (typeof __tl_user_event__ !== 'undefined') { + return __tl_user_event__.keyboard(text) + } + const { unreleased } = await triggerCommand<{ unreleased: string[] }>( + '__vitest_keyboard', + text, + keyboard, + ) + keyboard.unreleased = unreleased + }) }, } } @@ -170,7 +175,7 @@ export const page: BrowserPage = { const name = options.path || `${taskName.replace(/[^a-z0-9]/gi, '-')}-${number}.png` - return ensureAwaited(triggerCommand('__vitest_screenshot', name, { + return ensureAwaited(() => triggerCommand('__vitest_screenshot', name, { ...options, element: options.element ? convertToSelector(options.element) diff --git a/packages/browser/src/client/tester/locators/index.ts b/packages/browser/src/client/tester/locators/index.ts index 8121c9b38b0d..f87a94fc945b 100644 --- a/packages/browser/src/client/tester/locators/index.ts +++ b/packages/browser/src/client/tester/locators/index.ts @@ -202,7 +202,7 @@ export abstract class Locator { || this.worker.current?.file?.filepath || undefined - return ensureAwaited(this.rpc.triggerCommand( + return ensureAwaited(() => this.rpc.triggerCommand( this.state.contextId, command, filepath, diff --git a/packages/browser/src/client/tester/locators/preview.ts b/packages/browser/src/client/tester/locators/preview.ts index 74a367d15b75..0e966a8557c7 100644 --- a/packages/browser/src/client/tester/locators/preview.ts +++ b/packages/browser/src/client/tester/locators/preview.ts @@ -58,28 +58,28 @@ class PreviewLocator extends Locator { } click(): Promise { - return ensureAwaited(userEvent.click(this.element())) + return ensureAwaited(() => userEvent.click(this.element())) } dblClick(): Promise { - return ensureAwaited(userEvent.dblClick(this.element())) + return ensureAwaited(() => userEvent.dblClick(this.element())) } tripleClick(): Promise { - return ensureAwaited(userEvent.tripleClick(this.element())) + return ensureAwaited(() => userEvent.tripleClick(this.element())) } hover(): Promise { - return ensureAwaited(userEvent.hover(this.element())) + return ensureAwaited(() => userEvent.hover(this.element())) } unhover(): Promise { - return ensureAwaited(userEvent.unhover(this.element())) + return ensureAwaited(() => userEvent.unhover(this.element())) } async fill(text: string): Promise { await this.clear() - return ensureAwaited(userEvent.type(this.element(), text)) + return ensureAwaited(() => userEvent.type(this.element(), text)) } async upload(file: string | string[] | File | File[]): Promise { @@ -100,7 +100,7 @@ class PreviewLocator extends Locator { return fileInstance }) const uploadFiles = await Promise.all(uploadPromise) - return ensureAwaited(userEvent.upload(this.element() as HTMLElement, uploadFiles)) + return ensureAwaited(() => userEvent.upload(this.element() as HTMLElement, uploadFiles)) } selectOptions(options_: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise { @@ -110,7 +110,7 @@ class PreviewLocator extends Locator { } return option }) - return ensureAwaited(userEvent.selectOptions(this.element(), options as string[] | HTMLElement[])) + return ensureAwaited(() => userEvent.selectOptions(this.element(), options as string[] | HTMLElement[])) } async dropTo(): Promise { @@ -118,7 +118,7 @@ class PreviewLocator extends Locator { } clear(): Promise { - return ensureAwaited(userEvent.clear(this.element())) + return ensureAwaited(() => userEvent.clear(this.element())) } async screenshot(): Promise { diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index 1856c0f9597b..6a3fadd06d23 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -25,10 +25,10 @@ export function getConfig(): SerializedConfig { return getBrowserState().config } -export function ensureAwaited(promise: Promise): Promise { +export function ensureAwaited(promise: () => Promise): Promise { const test = getWorkerState().current if (!test || test.type !== 'test') { - return promise + return promise() } let awaited = false const sourceError = new Error('STACK_TRACE_ERROR') @@ -42,16 +42,18 @@ export function ensureAwaited(promise: Promise): Promise { throw error } }) + // don't even start the promise if it's not awaited to not cause any unhanded promise rejections + let promiseResult: Promise | undefined return { then(onFulfilled, onRejected) { awaited = true - return promise.then(onFulfilled, onRejected) + return (promiseResult ||= promise()).then(onFulfilled, onRejected) }, catch(onRejected) { - return promise.catch(onRejected) + return (promiseResult ||= promise()).catch(onRejected) }, finally(onFinally) { - return promise.finally(onFinally) + return (promiseResult ||= promise()).finally(onFinally) }, [Symbol.toStringTag]: 'Promise', } satisfies Promise diff --git a/packages/vitest/src/integrations/chai/poll.ts b/packages/vitest/src/integrations/chai/poll.ts index 237f4b607d12..859c9877aa7d 100644 --- a/packages/vitest/src/integrations/chai/poll.ts +++ b/packages/vitest/src/integrations/chai/poll.ts @@ -64,7 +64,7 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { return function (this: any, ...args: any[]) { const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR') - const promise = new Promise((resolve, reject) => { + const promise = () => new Promise((resolve, reject) => { let intervalId: any let lastError: any const { setTimeout, clearTimeout } = getSafeTimers() @@ -103,23 +103,24 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { const name = chai.util.flag(assertion, '_poll.element') ? 'element(locator)' : 'poll(assertion)' const assertionString = `expect.${name}.${negated}${String(key)}()` const error = new Error( - `${assertionString} was not awaited. This assertion is asynchronous and must be awaited:\n\nawait ${assertionString}\n`, + `${assertionString} was not awaited. This assertion is asynchronous and must be awaited, otherwise it is not executed to avoid unhandled rejections:\n\nawait ${assertionString}\n`, ) throw copyStackTrace(error, STACK_TRACE_ERROR) } }) + let resultPromise: Promise | undefined // only .then is enough to check awaited, but we type this as `Promise` in global types // so let's follow it return { then(onFulfilled, onRejected) { awaited = true - return promise.then(onFulfilled, onRejected) + return (resultPromise ||= promise()).then(onFulfilled, onRejected) }, catch(onRejected) { - return promise.catch(onRejected) + return (resultPromise ||= promise()).catch(onRejected) }, finally(onFinally) { - return promise.finally(onFinally) + return (resultPromise ||= promise()).finally(onFinally) }, [Symbol.toStringTag]: 'Promise', } satisfies Promise diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index ea49e1623ac1..c104d959b505 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -127,10 +127,9 @@ error with a stack expect(stderr).toMatch(/throwError src\/error.ts:8:(18|8)/) expect(stderr).toContain('The call was not awaited. This method is asynchronous and must be awaited.') - expect(stderr).toMatch(/test\/failing.test.ts:18:(27|33)/) - expect(stderr).toMatch(/test\/failing.test.ts:22:(27|36)/) - expect(stderr).toMatch(/test\/failing.test.ts:23:(27|39)/) - expect(stderr).toMatch(/test\/failing.test.ts:24:(27|33)/) + expect(stderr).toMatch(/test\/failing.test.ts:18:(27|36)/) + expect(stderr).toMatch(/test\/failing.test.ts:19:(27|39)/) + expect(stderr).toMatch(/test\/failing.test.ts:20:(27|33)/) }) test('popup apis should log a warning', () => { diff --git a/test/browser/test/failing.test.ts b/test/browser/test/failing.test.ts index dc53772533ef..96495127452e 100644 --- a/test/browser/test/failing.test.ts +++ b/test/browser/test/failing.test.ts @@ -14,12 +14,8 @@ it('correctly print error in another file', () => { throwError() }) -it('locator method is not awaited', () => { - page.getByRole('button').click() -}) - -it('several locator methods are not awaited', async () => { +it('several locator methods are not awaited', () => { page.getByRole('button').dblClick() - page.getByRole('button').tripleClick() page.getByRole('button').click() + page.getByRole('button').tripleClick() }) From a80b56beb35ae741757672d6596b60485e14a384 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 13 Nov 2024 12:29:46 +0100 Subject: [PATCH 15/16] test: fix tests --- packages/browser/src/client/utils.ts | 2 +- packages/vitest/src/integrations/chai/poll.ts | 2 +- test/browser/specs/runner.test.ts | 2 +- test/cli/test/__snapshots__/fails.test.ts.snap | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index 6a3fadd06d23..147b7c3dc344 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -36,7 +36,7 @@ export function ensureAwaited(promise: () => Promise): Promise { test.onFinished.push(() => { if (!awaited) { const error = new Error( - `The call was not awaited. This method is asynchronous and must be awaited.`, + `The call was not awaited. This method is asynchronous and must be awaited; otherwise, the call will not start to avoid unhandled rejections.`, ) error.stack = sourceError.stack?.replace(sourceError.message, error.message) throw error diff --git a/packages/vitest/src/integrations/chai/poll.ts b/packages/vitest/src/integrations/chai/poll.ts index 859c9877aa7d..4ee87549758a 100644 --- a/packages/vitest/src/integrations/chai/poll.ts +++ b/packages/vitest/src/integrations/chai/poll.ts @@ -103,7 +103,7 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] { const name = chai.util.flag(assertion, '_poll.element') ? 'element(locator)' : 'poll(assertion)' const assertionString = `expect.${name}.${negated}${String(key)}()` const error = new Error( - `${assertionString} was not awaited. This assertion is asynchronous and must be awaited, otherwise it is not executed to avoid unhandled rejections:\n\nawait ${assertionString}\n`, + `${assertionString} was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections:\n\nawait ${assertionString}\n`, ) throw copyStackTrace(error, STACK_TRACE_ERROR) } diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index c104d959b505..369712b3a0a3 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -126,7 +126,7 @@ error with a stack // column is 18 in safari, 8 in others expect(stderr).toMatch(/throwError src\/error.ts:8:(18|8)/) - expect(stderr).toContain('The call was not awaited. This method is asynchronous and must be awaited.') + expect(stderr).toContain('The call was not awaited. This method is asynchronous and must be awaited; otherwise, the call will not start to avoid unhandled rejections.') expect(stderr).toMatch(/test\/failing.test.ts:18:(27|36)/) expect(stderr).toMatch(/test\/failing.test.ts:19:(27|39)/) expect(stderr).toMatch(/test\/failing.test.ts:20:(27|33)/) diff --git a/test/cli/test/__snapshots__/fails.test.ts.snap b/test/cli/test/__snapshots__/fails.test.ts.snap index 952e7db3a2e8..8b6c55f29a16 100644 --- a/test/cli/test/__snapshots__/fails.test.ts.snap +++ b/test/cli/test/__snapshots__/fails.test.ts.snap @@ -61,12 +61,12 @@ exports[`should fail no-assertions.test.ts > no-assertions.test.ts 1`] = `"Error exports[`should fail node-browser-context.test.ts > node-browser-context.test.ts 1`] = `"Error: @vitest/browser/context can be imported only inside the Browser Mode. Your test is running in forks pool. Make sure your regular tests are excluded from the "test.include" glob pattern."`; exports[`should fail poll-no-awaited.test.ts > poll-no-awaited.test.ts 1`] = ` -"Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: +"Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections: AssertionError: expected 3 to be 4 // Object.is equality -Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: -Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited: -Error: expect.poll(assertion).not.toBe() was not awaited. This assertion is asynchronous and must be awaited: -Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited:" +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections: +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections: +Error: expect.poll(assertion).not.toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections: +Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections:" `; exports[`should fail primitive-error.test.ts > primitive-error.test.ts 1`] = `"Unknown Error: 42"`; From 269a4b7feed9d192cac43b5707f9b3b7dc81653a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 13 Nov 2024 12:35:39 +0100 Subject: [PATCH 16/16] test: fix safari --- test/browser/specs/runner.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 369712b3a0a3..64b8a94fc632 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -128,8 +128,8 @@ error with a stack expect(stderr).toContain('The call was not awaited. This method is asynchronous and must be awaited; otherwise, the call will not start to avoid unhandled rejections.') expect(stderr).toMatch(/test\/failing.test.ts:18:(27|36)/) - expect(stderr).toMatch(/test\/failing.test.ts:19:(27|39)/) - expect(stderr).toMatch(/test\/failing.test.ts:20:(27|33)/) + expect(stderr).toMatch(/test\/failing.test.ts:19:(27|33)/) + expect(stderr).toMatch(/test\/failing.test.ts:20:(27|39)/) }) test('popup apis should log a warning', () => {