diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts
index 45c4ad89c7..f4d31580bb 100644
--- a/packages/playwright-core/src/client/locator.ts
+++ b/packages/playwright-core/src/client/locator.ts
@@ -221,7 +221,7 @@ export class Locator implements api.Locator {
});
}
- async _expect(expression: string, options: FrameExpectOptions): Promise<{ pass: boolean, received?: any, log?: string[] }> {
+ async _expect(expression: string, options: FrameExpectOptions): Promise<{ matches: boolean, received?: any, log?: string[] }> {
return this._frame._wrapApiCall(async (channel: channels.FrameChannel) => {
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
if (options.expectedValue)
diff --git a/packages/playwright-core/src/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/dispatchers/frameDispatcher.ts
index f643174bbc..914cc7e9c1 100644
--- a/packages/playwright-core/src/dispatchers/frameDispatcher.ts
+++ b/packages/playwright-core/src/dispatchers/frameDispatcher.ts
@@ -232,7 +232,7 @@ export class FrameDispatcher extends Dispatcher = (injectedScript: js.JSHandle) => Promise>>;
-export type DomTaskBody = (progress: InjectedScriptProgress, element: E, data: T, elements: Element[], continuePolling: any) => R;
+export type DomTaskBody = (progress: InjectedScriptProgress, element: E, data: T, elements: Element[], continuePolling: symbol) => R | symbol;
export class FrameManager {
private _page: Page;
@@ -1161,26 +1161,47 @@ export class Frame extends SdkObject {
});
}
- async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ pass: boolean, received?: any, log?: string[] }> {
+ async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[] }> {
const controller = new ProgressController(metadata, this);
- const isListMatcher = options.expression.endsWith('.array');
- const querySelectorAll = options.expression === 'to.have.count' || isListMatcher;
+ const querySelectorAll = options.expression === 'to.have.count' || options.expression.endsWith('.array');
const mainWorld = options.expression === 'to.have.property';
- const expectsEmptyList = options.expectedText?.length === 0;
- const omitAttached = (isListMatcher && options.isNot !== expectsEmptyList) || (!options.isNot && options.expression === 'to.be.hidden') || (options.isNot && options.expression === 'to.be.visible');
return await this._scheduleRerunnableTaskWithController(controller, selector, (progress, element, options, elements, continuePolling) => {
- // We don't have an element and we don't need an element => pass.
- if (!element && options.omitAttached)
- return { pass: !options.isNot };
- // We don't have an element and we DO need an element => fail.
- if (!element)
- return { pass: !!options.isNot };
- // We have an element.
- return progress.injectedScript.expect(progress, element!, options, elements, continuePolling);
- }, { omitAttached, ...options }, { strict: true, querySelectorAll, mainWorld, omitAttached, logScale: true, ...options }).catch(e => {
+ if (!element) {
+ // expect(locator).toBeHidden() passes when there is no element.
+ if (!options.isNot && options.expression === 'to.be.hidden')
+ return { matches: true };
+
+ // expect(locator).not.toBeVisible() passes when there is no element.
+ if (options.isNot && options.expression === 'to.be.visible')
+ return { matches: false };
+
+ // expect(listLocator).toHaveText([]) passes when there are no elements matching.
+ // expect(listLocator).not.toHaveText(['foo']) passes when there are no elements matching.
+ const expectsEmptyList = options.expectedText?.length === 0;
+ if (options.expression.endsWith('.array') && expectsEmptyList !== options.isNot)
+ return { matches: expectsEmptyList };
+
+ // When none of the above applies, keep waiting for the element.
+ return continuePolling;
+ }
+
+ const { matches, received } = progress.injectedScript.expect(progress, element, options, elements);
+ if (matches === options.isNot) {
+ // Keep waiting in these cases:
+ // expect(locator).conditionThatDoesNotMatch
+ // expect(locator).not.conditionThatDoesMatch
+ progress.setIntermediateResult(received);
+ if (!Array.isArray(received))
+ progress.log(` unexpected value "${received}"`);
+ return continuePolling;
+ }
+
+ // Reached the expected state!
+ return { matches, received };
+ }, options, { strict: true, querySelectorAll, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => {
if (js.isJavaScriptErrorInEvaluate(e))
throw e;
- return { received: controller.lastIntermediateResult(), pass: !!options.isNot, log: metadata.log };
+ return { received: controller.lastIntermediateResult(), matches: options.isNot, log: metadata.log };
});
}
diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts
index 74c24ffdc8..672a8be9dd 100644
--- a/packages/playwright-core/src/server/injected/injectedScript.ts
+++ b/packages/playwright-core/src/server/injected/injectedScript.ts
@@ -775,7 +775,7 @@ export class InjectedScript {
return error;
}
- expect(progress: InjectedScriptProgress, element: Element, options: FrameExpectParams, elements: Element[], continuePolling: any): { pass: boolean, received?: any } {
+ expect(progress: InjectedScriptProgress, element: Element, options: FrameExpectParams, elements: Element[]): { matches: boolean, received?: any } {
const injected = progress.injectedScript;
const expression = options.expression;
@@ -808,12 +808,7 @@ export class InjectedScript {
throw injected.createStacklessError('Element is not a checkbox');
if (elementState === 'error:notconnected')
throw injected.createStacklessError('Element is not connected');
- if (elementState === options.isNot) {
- progress.setIntermediateResult(elementState);
- progress.log(` unexpected value "${elementState}"`);
- return continuePolling;
- }
- return { pass: !options.isNot };
+ return { received: elementState, matches: elementState };
}
}
@@ -822,12 +817,7 @@ export class InjectedScript {
if (expression === 'to.have.count') {
const received = elements.length;
const matches = received === options.expectedNumber;
- if (matches === options.isNot) {
- progress.setIntermediateResult(received);
- progress.log(` unexpected value "${received}"`);
- return continuePolling;
- }
- return { pass: !options.isNot, received };
+ return { received, matches };
}
}
@@ -836,12 +826,7 @@ export class InjectedScript {
if (expression === 'to.have.property') {
const received = (element as any)[options.expressionArg];
const matches = deepEquals(received, options.expectedValue);
- if (matches === options.isNot) {
- progress.setIntermediateResult(received);
- progress.log(` unexpected value "${received}"`);
- return continuePolling;
- }
- return { received, pass: !options.isNot };
+ return { received, matches };
}
}
@@ -870,12 +855,7 @@ export class InjectedScript {
if (received !== undefined && options.expectedText) {
const matcher = new ExpectedTextMatcher(options.expectedText[0]);
- if (matcher.matches(received) === options.isNot) {
- progress.setIntermediateResult(received);
- progress.log(` unexpected value "${received}"`);
- return continuePolling;
- }
- return { received, pass: !options.isNot };
+ return { received, matches: matcher.matches(received) };
}
}
@@ -891,12 +871,8 @@ export class InjectedScript {
// "To match an array" is "to contain an array" + "equal length"
const lengthShouldMatch = expression !== 'to.contain.text.array';
const matchesLength = received.length === options.expectedText.length || !lengthShouldMatch;
- if (matchesLength === options.isNot) {
- progress.setIntermediateResult(received);
- return continuePolling;
- }
if (!matchesLength)
- return { received, pass: !options.isNot };
+ return { received, matches: false };
// Each matcher should get a "received" that matches it, in order.
let i = 0;
@@ -910,11 +886,7 @@ export class InjectedScript {
break;
}
}
- if (allMatchesFound === options.isNot) {
- progress.setIntermediateResult(received);
- return continuePolling;
- }
- return { received, pass: !options.isNot };
+ return { received, matches: allMatchesFound };
}
}
throw this.createStacklessError('Unknown expect matcher: ' + options.expression);
diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts
index af9be234c2..e86682e152 100644
--- a/packages/playwright-test/src/matchers/matchers.ts
+++ b/packages/playwright-test/src/matchers/matchers.ts
@@ -23,7 +23,7 @@ import { toEqual } from './toEqual';
import { toExpectedTextValues, toMatchText } from './toMatchText';
interface LocatorEx extends Locator {
- _expect(expression: string, options: FrameExpectOptions): Promise<{ pass: boolean, received?: any, log?: string[] }>;
+ _expect(expression: string, options: FrameExpectOptions): Promise<{ matches: boolean, received?: any, log?: string[] }>;
}
export function toBeChecked(
diff --git a/packages/playwright-test/src/matchers/toBeTruthy.ts b/packages/playwright-test/src/matchers/toBeTruthy.ts
index 18ffee4bd8..226cb284e1 100644
--- a/packages/playwright-test/src/matchers/toBeTruthy.ts
+++ b/packages/playwright-test/src/matchers/toBeTruthy.ts
@@ -24,7 +24,7 @@ export async function toBeTruthy(
matcherName: string,
receiver: any,
receiverType: string,
- query: (isNot: boolean, timeout: number) => Promise<{ pass: boolean, log?: string[] }>,
+ query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, log?: string[] }>,
options: { timeout?: number } = {},
) {
const testInfo = currentTestInfo();
@@ -42,11 +42,11 @@ export async function toBeTruthy(
defaultExpectTimeout = 5000;
const timeout = options.timeout === 0 ? 0 : options.timeout || defaultExpectTimeout;
- const { pass, log } = await query(this.isNot, timeout);
+ const { matches, log } = await query(this.isNot, timeout);
const message = () => {
return this.utils.matcherHint(matcherName, undefined, '', matcherOptions) + callLogText(log);
};
- return { message, pass };
+ return { message, pass: matches };
}
diff --git a/packages/playwright-test/src/matchers/toEqual.ts b/packages/playwright-test/src/matchers/toEqual.ts
index 34a5c0f417..9c12242c08 100644
--- a/packages/playwright-test/src/matchers/toEqual.ts
+++ b/packages/playwright-test/src/matchers/toEqual.ts
@@ -31,7 +31,7 @@ export async function toEqual(
matcherName: string,
receiver: any,
receiverType: string,
- query: (isNot: boolean, timeout: number) => Promise<{ pass: boolean, received?: any, log?: string[] }>,
+ query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: any, log?: string[] }>,
expected: T,
options: { timeout?: number, contains?: boolean } = {},
) {
@@ -51,7 +51,7 @@ export async function toEqual(
defaultExpectTimeout = 5000;
const timeout = options.timeout === 0 ? 0 : options.timeout || defaultExpectTimeout;
- const { pass, received, log } = await query(this.isNot, timeout);
+ const { matches: pass, received, log } = await query(this.isNot, timeout);
const message = pass
? () =>
diff --git a/packages/playwright-test/src/matchers/toMatchText.ts b/packages/playwright-test/src/matchers/toMatchText.ts
index 77d615185e..e45faddcac 100644
--- a/packages/playwright-test/src/matchers/toMatchText.ts
+++ b/packages/playwright-test/src/matchers/toMatchText.ts
@@ -31,7 +31,7 @@ export async function toMatchText(
matcherName: string,
receiver: any,
receiverType: string,
- query: (isNot: boolean, timeout: number) => Promise<{ pass: boolean, received?: string, log?: string[] }>,
+ query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: string, log?: string[] }>,
expected: string | RegExp,
options: { timeout?: number, matchSubstring?: boolean } = {},
) {
@@ -65,7 +65,7 @@ export async function toMatchText(
defaultExpectTimeout = 5000;
const timeout = options.timeout === 0 ? 0 : options.timeout || defaultExpectTimeout;
- const { pass, received, log } = await query(this.isNot, timeout);
+ const { matches: pass, received, log } = await query(this.isNot, timeout);
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
const receivedString = received || '';
const message = pass
diff --git a/tests/playwright-test/playwright.expect.text.spec.ts b/tests/playwright-test/playwright.expect.text.spec.ts
index ab1ad11c76..b4d1a88272 100644
--- a/tests/playwright-test/playwright.expect.text.spec.ts
+++ b/tests/playwright-test/playwright.expect.text.spec.ts
@@ -184,6 +184,12 @@ test('should support toHaveText w/ array', async ({ runInlineTest }) => {
await expect(locator).not.toHaveText(['Test']);
});
+ test('fail on not+empty', async ({ page }) => {
+ await page.setContent('');
+ const locator = page.locator('p');
+ await expect(locator).not.toHaveText([], { timeout: 1000 });
+ });
+
test('pass eventually empty', async ({ page }) => {
await page.setContent('');
const locator = page.locator('p');
@@ -207,7 +213,7 @@ test('should support toHaveText w/ array', async ({ runInlineTest }) => {
expect(output).toContain('waiting for selector "div"');
expect(output).toContain('selector resolved to 2 elements');
expect(result.passed).toBe(6);
- expect(result.failed).toBe(1);
+ expect(result.failed).toBe(2);
expect(result.exitCode).toBe(1);
});