chore(expect): simplify expect implementation (#9459)
This commit is contained in:
parent
437caa35ad
commit
64a3099655
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
|
|||
const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue });
|
||||
if (result.received !== undefined)
|
||||
result.received = serializeResult(result.received);
|
||||
if (result.pass === !!params.isNot)
|
||||
if (result.matches === params.isNot)
|
||||
metadata.error = { error: { name: 'Expect', message: 'Expect failed' } };
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2233,7 +2233,7 @@ export type FrameExpectOptions = {
|
|||
timeout?: number,
|
||||
};
|
||||
export type FrameExpectResult = {
|
||||
pass: boolean,
|
||||
matches: boolean,
|
||||
received?: SerializedValue,
|
||||
log?: string[],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1802,7 +1802,7 @@ Frame:
|
|||
isNot: boolean
|
||||
timeout: number?
|
||||
returns:
|
||||
pass: boolean
|
||||
matches: boolean
|
||||
received: SerializedValue?
|
||||
log:
|
||||
type: array?
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export type NavigationEvent = {
|
|||
};
|
||||
|
||||
export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<InjectedScriptPoll<T>>>;
|
||||
export type DomTaskBody<T, R, E> = (progress: InjectedScriptProgress, element: E, data: T, elements: Element[], continuePolling: any) => R;
|
||||
export type DomTaskBody<T, R, E> = (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 };
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export async function toEqual<T>(
|
|||
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<T>(
|
|||
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
|
||||
? () =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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('<div></div>');
|
||||
const locator = page.locator('p');
|
||||
await expect(locator).not.toHaveText([], { timeout: 1000 });
|
||||
});
|
||||
|
||||
test('pass eventually empty', async ({ page }) => {
|
||||
await page.setContent('<div id=div><p>Text</p></div>');
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue