From 9cfbc0c171cc66ad9510f54b81e9d0de1b191d1c Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 1 Nov 2021 16:42:13 -0700 Subject: [PATCH] chore(expect): simplify expect array edge cases (#9942) --- packages/playwright-core/src/server/frames.ts | 52 ++++++------- .../src/server/injected/injectedScript.ts | 77 ++++++++++--------- 2 files changed, 61 insertions(+), 68 deletions(-) diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 065f622559..bae8a17b6d 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -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? diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 077257a972..f0be9a0e6c 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -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); } }