diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 22f424d7f2..cdcb44007f 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1402,7 +1402,7 @@ export class Frame extends SdkObject { timeout -= elapsed; } if (timeout < 0) - return { matches: options.isNot, log: metadata.log, timedOut: true }; + return { matches: options.isNot, log: metadata.log, timedOut: true, received: lastIntermediateResult.received }; return await this._expectInternal(metadata, selector, options, false, timeout, lastIntermediateResult); } @@ -1422,7 +1422,7 @@ export class Frame extends SdkObject { const injected = await context.injectedScript(); progress.throwIfAborted(); - const { log, matches, received } = await injected.evaluate(async (injected, { info, options, snapshotName }) => { + const { log, matches, received, missingRecevied } = await injected.evaluate(async (injected, { info, options, snapshotName }) => { const elements = info ? injected.querySelectorAll(info.parsed, document) : []; const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array'); let log = ''; @@ -1439,7 +1439,8 @@ export class Frame extends SdkObject { if (log) progress.log(log); - if (matches === options.isNot) { + // Note: missingReceived avoids `unexpected value "undefined"` when element was not found. + if (matches === options.isNot && !missingRecevied) { lastIntermediateResult.received = received; lastIntermediateResult.isSet = true; if (!Array.isArray(received)) diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 2ce835e5d1..fb16c64ea3 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -1119,7 +1119,7 @@ export class InjectedScript { this.onGlobalListenersRemoved.add(addHitTargetInterceptorListeners); } - async expect(element: Element | undefined, options: FrameExpectParams, elements: Element[]) { + async expect(element: Element | undefined, options: FrameExpectParams, elements: Element[]): Promise<{ matches: boolean, received?: any, missingRecevied?: boolean }> { const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array'); if (isArray) return this.expectArray(elements, options); @@ -1134,7 +1134,7 @@ export class InjectedScript { if (options.isNot && options.expression === 'to.be.in.viewport') return { matches: false }; // When none of the above applies, expect does not match. - return { matches: options.isNot }; + return { matches: options.isNot, missingRecevied: true }; } return await this.expectSingleElement(element, options); } diff --git a/tests/page/expect-to-have-text.spec.ts b/tests/page/expect-to-have-text.spec.ts index 9b83b65235..d76051feb4 100644 --- a/tests/page/expect-to-have-text.spec.ts +++ b/tests/page/expect-to-have-text.spec.ts @@ -128,6 +128,13 @@ test.describe('toHaveText with text', () => { await expect(page.locator('div')).not.toHaveText('some text', { useInnerText: true }); await expect(page.locator('div')).not.toContainText('text', { useInnerText: true }); }); + + test('fail with impossible timeout', async ({ page }) => { + await page.setContent('