diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index f4d31580bb..a35276bcac 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -36,14 +36,18 @@ export class Locator implements api.Locator { private async _withElement(task: (handle: ElementHandle, timeout?: number) => Promise, timeout?: number): Promise { timeout = this._frame.page()._timeoutSettings.timeout({ timeout }); const deadline = timeout ? monotonicTime() + timeout : 0; - const handle = await this.elementHandle({ timeout }); - if (!handle) - throw new Error(`Could not resolve ${this._selector} to DOM Element`); - try { - return await task(handle, deadline ? deadline - monotonicTime() : 0); - } finally { - await handle.dispose(); - } + + return this._frame._wrapApiCall(async (channel: channels.FrameChannel) => { + const result = await channel.waitForSelector({ selector: this._selector, strict: true, state: 'attached', timeout }); + const handle = ElementHandle.fromNullable(result.element) as ElementHandle | null; + if (!handle) + throw new Error(`Could not resolve ${this._selector} to DOM Element`); + try { + return await task(handle, deadline ? deadline - monotonicTime() : 0); + } finally { + await handle.dispose(); + } + }); } async boundingBox(options?: TimeoutOptions): Promise { diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index 8529a083fa..932aa324e6 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -368,6 +368,43 @@ test('should not have internal error when steps are finished after timeout', asy expect(result.output).not.toContain('Internal error'); }); +test('should show nice stacks for locators', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': stepsReporterJS, + 'playwright.config.ts': ` + module.exports = { + reporter: './reporter', + }; + `, + 'a.test.ts': ` + const { test } = pwt; + test('pass', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('button'); + await locator.evaluate(e => e.innerText); + }); + ` + }, { reporter: '', workers: 1 }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(0); + expect(result.output).not.toContain('Internal error'); + expect(result.output.split('\n').filter(line => line.startsWith('%%')).map(stripEscapedAscii)).toEqual([ + `%% begin {"title":"Before Hooks","category":"hook"}`, + `%% begin {"title":"browserContext.newPage","category":"pw:api"}`, + `%% end {"title":"browserContext.newPage","category":"pw:api"}`, + `%% end {"title":"Before Hooks","category":"hook","steps":[{"title":"browserContext.newPage","category":"pw:api"}]}`, + `%% begin {"title":"page.setContent","category":"pw:api"}`, + `%% end {"title":"page.setContent","category":"pw:api"}`, + `%% begin {"title":"locator.evaluate(button)","category":"pw:api"}`, + `%% end {"title":"locator.evaluate(button)","category":"pw:api"}`, + `%% begin {"title":"After Hooks","category":"hook"}`, + `%% begin {"title":"browserContext.close","category":"pw:api"}`, + `%% end {"title":"browserContext.close","category":"pw:api"}`, + `%% end {"title":"After Hooks","category":"hook","steps":[{"title":"browserContext.close","category":"pw:api"}]}`, + ]); +}); + function stripEscapedAscii(str: string) { return str.replace(/\\u00[a-z0-9][a-z0-9]\[[^m]+m/g, ''); }