diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 7395e38217..6a32d92876 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -25,6 +25,7 @@ import { wrapFunctionWithLocation } from '../transform/transform'; import type { FixturesWithLocation } from './config'; import type { Fixtures, TestDetails, TestStepInfo, TestType } from '../../types/test'; import type { Location } from '../../types/testReporter'; +import type { TestInfoImpl } from '../worker/testInfo'; const testTypeSymbol = Symbol('testType'); @@ -261,10 +262,14 @@ export class TestTypeImpl { suite._use.push({ fixtures, location }); } - async _step(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise { + _step(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise { const testInfo = currentTestInfo(); if (!testInfo) throw new Error(`test.step() can only be called from a test`); + return testInfo._wrapPromiseAPIResult(this._stepInternal(expectation, testInfo, title, body, options)); + } + + async _stepInternal(expectation: 'pass'|'skip', testInfo: TestInfoImpl, title: string, body: (step: TestStepInfo) => T | Promise, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise { const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); return await zones.run('stepZone', step, async () => { try { diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 48a0b801d8..4c048456a9 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -383,19 +383,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { const result = zones.run('stepZone', step, callback); if (result instanceof Promise) { const promise = result.then(finalizer).catch(reportStepError); - - testInfo?.unusedAsyncApiCalls.add(promise); - - const oldThen = promise.then; - promise.then = ((...args: any[]) => { - if (args[0] !== undefined) { - // onfulfilled callback - testInfo?.unusedAsyncApiCalls.delete(promise); - } - return oldThen.call(promise, ...args); - }) as any; - - return promise; + return testInfo._wrapPromiseAPIResult(promise); } finalizer(); return result; diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 9ba4a7f7b2..7f693bab17 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -411,6 +411,26 @@ export class TestInfoImpl implements TestInfo { this._timeoutManager.setIgnoreTimeouts(); } + /** + * Enables a promise API call to be tracked by the test, alerting if unawaited. + * + * **NOTE:** Returning from an async function wraps the result in a promise, regardless of whether the return value is a promise. This will automatically mark the promise as awaited. Avoid this. + */ + _wrapPromiseAPIResult(promise: Promise): Promise { + this.unusedAsyncApiCalls.add(promise); + + const oldThen = promise.then; + promise.then = ((...args: any[]) => { + if (args[0] !== undefined) { + // onfulfilled callback, which means .then() was called + this.unusedAsyncApiCalls.delete(promise); + } + return oldThen.call(promise, ...args); + }) as any; + + return promise; + } + // ------------ TestInfo methods ------------ async attach(name: string, options: { path?: string, body?: string | Buffer, contentType?: string } = {}) {