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[] }> {
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';
return await this._scheduleRerunnableTaskWithController(controller, selector, (progress, element, options, elements) => {
if (!element) {
// expect(locator).toBeHidden() passes when there is no element.
if (!options.isNot && options.expression === 'to.be.hidden')
return { matches: true };
let result: { matches: boolean, received?: any };
// 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 };
// expect(listLocator).toHaveCount(0) passes when there are no elements matching.
// expect(listLocator).not.toHaveCount(1) passes when there are no elements matching.
const expectsEmptyCount = options.expectedNumber === 0;
if (options.expression === 'to.have.count' && expectsEmptyCount !== options.isNot)
return { matches: expectsEmptyCount, received: 0 };
// When none of the above applies, keep waiting for the element.
return progress.continuePolling;
if (options.isArray) {
result = progress.injectedScript.expectArray(elements, options);
} else {
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 };
// When none of the above applies, keep waiting for the element.
return progress.continuePolling;
}
result = progress.injectedScript.expectSingleElement(progress, element, options);
}
const { matches, received } = progress.injectedScript.expect(progress, element, options, elements);
if (matches === options.isNot) {
if (result.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}"`);
progress.setIntermediateResult(result.received);
if (!Array.isArray(result.received))
progress.log(` unexpected value "${result.received}"`);
return progress.continuePolling;
}
// Reached the expected state!
return { matches, received };
}, options, { strict: true, querySelectorAll, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => {
return result;
}, { ...options, isArray }, { strict: true, querySelectorAll: isArray, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => {
if (js.isJavaScriptErrorInEvaluate(e))
throw e;
// Q: Why not throw upon isSessionClosedError(e) as in other places?

View File

@ -798,7 +798,7 @@ export class InjectedScript {
}).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 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
if (expression === 'to.have.property') {
@ -882,37 +873,47 @@ export class InjectedScript {
}
}
{
// 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);
throw this.createStacklessError('Unknown expect matcher: ' + expression);
}
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 };
expectArray(elements: Element[], options: FrameExpectParams): { matches: boolean, received?: any } {
const expression = options.expression;
// 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 };
}
if (expression === 'to.have.count') {
const received = elements.length;
const matches = received === options.expectedNumber;
return { received, matches };
}
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);
}
}