chore(expect): simplify expect implementation (#9459)

This commit is contained in:
Dmitry Gozman 2021-10-13 08:56:57 -07:00 committed by GitHub
parent 437caa35ad
commit 64a3099655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 63 additions and 64 deletions

View File

@ -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)

View File

@ -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;
}

View File

@ -2233,7 +2233,7 @@ export type FrameExpectOptions = {
timeout?: number,
};
export type FrameExpectResult = {
pass: boolean,
matches: boolean,
received?: SerializedValue,
log?: string[],
};

View File

@ -1802,7 +1802,7 @@ Frame:
isNot: boolean
timeout: number?
returns:
pass: boolean
matches: boolean
received: SerializedValue?
log:
type: array?

View File

@ -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 };
});
}

View File

@ -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);

View File

@ -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(

View File

@ -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 };
}

View File

@ -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
? () =>

View File

@ -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

View File

@ -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);
});