chore(expect): simplify expect array edge cases (#9942)

This commit is contained in:
Dmitry Gozman 2021-11-01 16:42:13 -07:00 committed by GitHub
parent 2bfbf65b8d
commit 9cfbc0c171
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 61 additions and 68 deletions

View File

@ -1156,48 +1156,40 @@ export class Frame extends SdkObject {
async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: 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 controller = new ProgressController(metadata, this);
const querySelectorAll = options.expression === 'to.have.count' || options.expression.endsWith('.array'); const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
const mainWorld = options.expression === 'to.have.property'; const mainWorld = options.expression === 'to.have.property';
return await this._scheduleRerunnableTaskWithController(controller, selector, (progress, element, options, elements) => { return await this._scheduleRerunnableTaskWithController(controller, selector, (progress, element, options, elements) => {
if (!element) { let result: { matches: boolean, received?: any };
// 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.isArray) {
if (options.isNot && options.expression === 'to.be.visible') result = progress.injectedScript.expectArray(elements, options);
return { matches: false }; } else {
if (!element) {
// expect(listLocator).toHaveText([]) passes when there are no elements matching. // expect(locator).toBeHidden() passes when there is no element.
// expect(listLocator).not.toHaveText(['foo']) passes when there are no elements matching. if (!options.isNot && options.expression === 'to.be.hidden')
const expectsEmptyList = options.expectedText?.length === 0; return { matches: true };
if (options.expression.endsWith('.array') && expectsEmptyList !== options.isNot) // expect(locator).not.toBeVisible() passes when there is no element.
return { matches: expectsEmptyList }; if (options.isNot && options.expression === 'to.be.visible')
return { matches: false };
// expect(listLocator).toHaveCount(0) passes when there are no elements matching. // When none of the above applies, keep waiting for the element.
// expect(listLocator).not.toHaveCount(1) passes when there are no elements matching. return progress.continuePolling;
const expectsEmptyCount = options.expectedNumber === 0; }
if (options.expression === 'to.have.count' && expectsEmptyCount !== options.isNot) result = progress.injectedScript.expectSingleElement(progress, element, options);
return { matches: expectsEmptyCount, received: 0 };
// When none of the above applies, keep waiting for the element.
return progress.continuePolling;
} }
const { matches, received } = progress.injectedScript.expect(progress, element, options, elements); if (result.matches === options.isNot) {
if (matches === options.isNot) {
// Keep waiting in these cases: // Keep waiting in these cases:
// expect(locator).conditionThatDoesNotMatch // expect(locator).conditionThatDoesNotMatch
// expect(locator).not.conditionThatDoesMatch // expect(locator).not.conditionThatDoesMatch
progress.setIntermediateResult(received); progress.setIntermediateResult(result.received);
if (!Array.isArray(received)) if (!Array.isArray(result.received))
progress.log(` unexpected value "${received}"`); progress.log(` unexpected value "${result.received}"`);
return progress.continuePolling; return progress.continuePolling;
} }
// Reached the expected state! // Reached the expected state!
return { matches, received }; return result;
}, options, { strict: true, querySelectorAll, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => { }, { ...options, isArray }, { strict: true, querySelectorAll: isArray, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => {
if (js.isJavaScriptErrorInEvaluate(e)) if (js.isJavaScriptErrorInEvaluate(e))
throw e; throw e;
// Q: Why not throw upon isSessionClosedError(e) as in other places? // Q: Why not throw upon isSessionClosedError(e) as in other places?

View File

@ -798,7 +798,7 @@ export class InjectedScript {
}).observe(document, { childList: true }); }).observe(document, { childList: true });
} }
expect(progress: InjectedScriptProgress, element: Element, options: FrameExpectParams, elements: Element[]): { matches: boolean, received?: any } { expectSingleElement(progress: InjectedScriptProgress, element: Element, options: FrameExpectParams): { matches: boolean, received?: any } {
const injected = progress.injectedScript; const injected = progress.injectedScript;
const expression = options.expression; const expression = options.expression;
@ -835,15 +835,6 @@ export class InjectedScript {
} }
} }
{
// Single number value.
if (expression === 'to.have.count') {
const received = elements.length;
const matches = received === options.expectedNumber;
return { received, matches };
}
}
{ {
// JS property // JS property
if (expression === 'to.have.property') { if (expression === 'to.have.property') {
@ -882,37 +873,47 @@ export class InjectedScript {
} }
} }
{ throw this.createStacklessError('Unknown expect matcher: ' + expression);
// List of values. }
let received: string[] | undefined;
if (expression === 'to.have.text.array' || expression === 'to.contain.text.array')
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : e.textContent || '');
else if (expression === 'to.have.class.array')
received = elements.map(e => e.className);
if (received && options.expectedText) { expectArray(elements: Element[], options: FrameExpectParams): { matches: boolean, received?: any } {
// "To match an array" is "to contain an array" + "equal length" const expression = options.expression;
const lengthShouldMatch = expression !== 'to.contain.text.array';
const matchesLength = received.length === options.expectedText.length || !lengthShouldMatch;
if (!matchesLength)
return { received, matches: false };
// Each matcher should get a "received" that matches it, in order. if (expression === 'to.have.count') {
let i = 0; const received = elements.length;
const matchers = options.expectedText.map(e => new ExpectedTextMatcher(e)); const matches = received === options.expectedNumber;
let allMatchesFound = true; return { received, matches };
for (const matcher of matchers) {
while (i < received.length && !matcher.matches(received[i]))
i++;
if (i >= received.length) {
allMatchesFound = false;
break;
}
}
return { received, matches: allMatchesFound };
}
} }
throw this.createStacklessError('Unknown expect matcher: ' + options.expression);
// List of values.
let received: string[] | undefined;
if (expression === 'to.have.text.array' || expression === 'to.contain.text.array')
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : e.textContent || '');
else if (expression === 'to.have.class.array')
received = elements.map(e => e.className);
if (received && options.expectedText) {
// "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)
return { received, matches: false };
// Each matcher should get a "received" that matches it, in order.
let i = 0;
const matchers = options.expectedText.map(e => new ExpectedTextMatcher(e));
let allMatchesFound = true;
for (const matcher of matchers) {
while (i < received.length && !matcher.matches(received[i]))
i++;
if (i >= received.length) {
allMatchesFound = false;
break;
}
}
return { received, matches: allMatchesFound };
}
throw this.createStacklessError('Unknown expect matcher: ' + expression);
} }
} }