diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index ecc8607b19..8670ebbea2 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -187,9 +187,7 @@ export abstract class ChannelOwner ${apiName} started`, isInternal); const apiZone: ApiZone = { apiName, frames, isInternal, reported: false, csi, callCookie, wallTime }; - const result = await zones.run>('apiZone', apiZone, async () => { - return await func(apiZone); - }); + const result = await zones.run('apiZone', apiZone, async () => await func(apiZone)); csi?.onApiCallEnd(callCookie); logApiCall(logger, `<= ${apiName} succeeded`, isInternal); return result; diff --git a/packages/playwright-core/src/utils/zones.ts b/packages/playwright-core/src/utils/zones.ts index a9cfed13bb..68cb6fa7fa 100644 --- a/packages/playwright-core/src/utils/zones.ts +++ b/packages/playwright-core/src/utils/zones.ts @@ -21,18 +21,18 @@ export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone'; class ZoneManager { private readonly _asyncLocalStorage = new AsyncLocalStorage|undefined>(); - run(type: ZoneType, data: T, func: (data: T) => R): R { + run(type: ZoneType, data: T, func: () => R): R { const previous = this._asyncLocalStorage.getStore(); const zone = new Zone(previous, type, data); - return this._asyncLocalStorage.run(zone, () => func(data)); + return this._asyncLocalStorage.run(zone, func); } - zoneData(type: ZoneType): T | null { + zoneData(type: ZoneType): T | undefined { for (let zone = this._asyncLocalStorage.getStore(); zone; zone = zone.previous) { if (zone.type === type) return zone.data as T; } - return null; + return undefined; } exitZones(func: () => R): R { diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index cc49a24d81..ef521ffc44 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -284,9 +284,12 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { }; try { - const expectZone: ExpectZone | null = matcherName !== 'toPass' ? { title, wallTime } : null; const callback = () => matcher.call(target, ...args); - const result = expectZone ? zones.run('expectZone', expectZone, callback) : callback(); + // toPass and poll matchers can contain other steps, expects and API calls, + // so they behave like a retriable step. + const result = (matcherName === 'toPass' || this._info.isPoll) ? + zones.run('stepZone', step, callback) : + zones.run('expectZone', { title, wallTime }, callback); if (result instanceof Promise) return result.then(finalizer).catch(reportStepError); finalizer(); diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 387e3bd5b5..5204ade21e 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -252,7 +252,7 @@ export class TestInfoImpl implements TestInfo { // Predefined stages form a fixed hierarchy - use the current one as parent. parentStep = this._findLastStageStep(); } else { - parentStep = zones.zoneData('stepZone') || undefined; + parentStep = zones.zoneData('stepZone'); if (!parentStep && data.category !== 'test.step') { // API steps (but not test.step calls) can be nested by time, instead of by stack. // However, do not nest chains of route.continue by checking the title. diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index 8be42de189..00efd0161a 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -846,7 +846,6 @@ fixture | fixture: context test('step inside expect.toPass', async ({ runInlineTest }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30322' }); - test.fixme(); const result = await runInlineTest({ 'reporter.ts': stepIndentReporter, 'playwright.config.ts': ` @@ -883,7 +882,7 @@ expect | expect.toPass @ a.test.ts:11 test.step | step 2, attempt: 0 @ a.test.ts:7 test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | expect.toBe @ a.test.ts:9 -expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality test.step | step 2, attempt: 1 @ a.test.ts:7 expect | expect.toBe @ a.test.ts:9 test.step | step 3 @ a.test.ts:12 @@ -895,7 +894,6 @@ hook |After Hooks test('library API call inside expect.toPass', async ({ runInlineTest }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30322' }); - test.fixme(); const result = await runInlineTest({ 'reporter.ts': stepIndentReporter, 'playwright.config.ts': ` @@ -940,4 +938,53 @@ hook |After Hooks fixture | fixture: page fixture | fixture: context `); -}); \ No newline at end of file +}); + +test('library API call inside expect.poll', async ({ runInlineTest }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30322' }); + const result = await runInlineTest({ + 'reporter.ts': stepIndentReporter, + 'playwright.config.ts': ` + module.exports = { + reporter: './reporter', + }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({page}) => { + let counter = 0 + const a = []; + await expect.poll(async () => { + await page.goto('about:blank'); + await test.step('inner step attempt: ' + counter, async () => { + counter++; + expect(1).toBe(1); + }); + a.push(1); + return a; + }).toHaveLength(2); + }); + ` + }, { reporter: '', workers: 1 }); + + expect(result.exitCode).toBe(0); + expect(stripAnsi(result.output)).toBe(` +hook |Before Hooks +fixture | fixture: browser +pw:api | browserType.launch +fixture | fixture: context +pw:api | browser.newContext +fixture | fixture: page +pw:api | browserContext.newPage +expect |expect.poll.toHaveLength @ a.test.ts:14 +pw:api | page.goto(about:blank) @ a.test.ts:7 +test.step | inner step attempt: 0 @ a.test.ts:8 +expect | expect.toBe @ a.test.ts:10 +pw:api | page.goto(about:blank) @ a.test.ts:7 +test.step | inner step attempt: 1 @ a.test.ts:8 +expect | expect.toBe @ a.test.ts:10 +hook |After Hooks +fixture | fixture: page +fixture | fixture: context +`); +});