diff --git a/packages/playwright/src/matchers/error.ts b/packages/playwright/src/matchers/error.ts deleted file mode 100644 index 12762a3378..0000000000 --- a/packages/playwright/src/matchers/error.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ExpectMatcherState } from '../../types/test'; -import { kNoElementsFoundError, matcherHint } from './matcherHint'; -import { colors } from 'playwright-core/lib/utilsBundle'; -import type { Locator } from 'playwright-core'; -import { EXPECTED_COLOR, printReceived } from '../common/expectBundle'; -import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring } from './expect'; -import { callLogText } from '../util'; - -export function toMatchExpectedStringOrPredicateVerification( - state: ExpectMatcherState, - matcherName: string, - receiver: Locator | undefined, - expression: string | Locator | undefined, - expected: string | RegExp | Function, - supportsPredicate: boolean = false -): void { - const matcherOptions = { - isNot: state.isNot, - promise: state.promise, - }; - - if ( - !(typeof expected === 'string') && - !(expected && 'test' in expected && typeof expected.test === 'function') && - !(supportsPredicate && typeof expected === 'function') - ) { - // Same format as jest's matcherErrorMessage - const message = supportsPredicate ? 'string, regular expression, or predicate' : 'string or regular expression'; - - throw new Error([ - // Always display `expected` in expectation place - matcherHint(state, receiver, matcherName, expression, undefined, matcherOptions), - `${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected',)} value must be a ${message}`, - state.utils.printWithType('Expected', expected, state.utils.printExpected) - ].join('\n\n')); - } -} - -export function textMatcherMessage(state: ExpectMatcherState, matcherName: string, receiver: Locator | undefined, expression: string, expected: string | RegExp | Function, received: string | undefined, callLog: string[] | undefined, stringName: string, pass: boolean, didTimeout: boolean, timeout: number): string { - const matcherOptions = { - isNot: state.isNot, - promise: state.promise, - }; - const receivedString = received || ''; - const messagePrefix = matcherHint(state, receiver, matcherName, expression, undefined, matcherOptions, didTimeout ? timeout : undefined); - const notFound = received === kNoElementsFoundError; - - let printedReceived: string | undefined; - let printedExpected: string | undefined; - let printedDiff: string | undefined; - if (typeof expected === 'function') { - printedExpected = `Expected predicate to ${!state.isNot ? 'succeed' : 'fail'}`; - printedReceived = `Received string: ${printReceived(receivedString)}`; - } else { - if (pass) { - if (typeof expected === 'string') { - if (notFound) { - printedExpected = `Expected ${stringName}: not ${state.utils.printExpected(expected)}`; - printedReceived = `Received: ${received}`; - } else { - printedExpected = `Expected ${stringName}: not ${state.utils.printExpected(expected)}`; - const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); - printedReceived = `Received string: ${formattedReceived}`; - } - } else { - if (notFound) { - printedExpected = `Expected pattern: not ${state.utils.printExpected(expected)}`; - printedReceived = `Received: ${received}`; - } else { - printedExpected = `Expected pattern: not ${state.utils.printExpected(expected)}`; - const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); - printedReceived = `Received string: ${formattedReceived}`; - } - } - } else { - const labelExpected = `Expected ${typeof expected === 'string' ? stringName : 'pattern'}`; - if (notFound) { - printedExpected = `${labelExpected}: ${state.utils.printExpected(expected)}`; - printedReceived = `Received: ${received}`; - } else { - printedDiff = state.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false); - } - } - } - - const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived; - return messagePrefix + resultDetails + callLogText(callLog); -} diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 8085269342..66a491a743 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -21,12 +21,12 @@ import { expectTypes, callLogText } from '../util'; import { toBeTruthy } from './toBeTruthy'; import { toEqual } from './toEqual'; import { toMatchText } from './toMatchText'; -import { isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues, urlMatches } from 'playwright-core/lib/utils'; +import { isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils'; import { currentTestInfo } from '../common/globals'; import { TestInfoImpl } from '../worker/testInfo'; import type { ExpectMatcherState } from '../../types/test'; import { takeFirst } from '../common/config'; -import { textMatcherMessage, toMatchExpectedStringOrPredicateVerification } from './error'; +import { toHaveURL as toHaveURLExternal } from './toHaveURL'; export interface LocatorEx extends Locator { _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>; @@ -393,79 +393,7 @@ export async function toHaveURL( expected: string | RegExp | ((url: URL) => boolean), options?: { ignoreCase?: boolean; timeout?: number }, ) { - const matcherName = 'toHaveURL'; - const expression = 'page'; - toMatchExpectedStringOrPredicateVerification( - this, - matcherName, - undefined, - expression, - expected, - true, - ); - - const timeout = options?.timeout ?? this.timeout; - let conditionSucceeded = false; - let lastCheckedURLString: string | undefined = undefined; - try { - await page.mainFrame().waitForURL( - url => { - const baseURL: string | undefined = (page.context() as any)._options - .baseURL; - lastCheckedURLString = url.toString(); - - if (options?.ignoreCase) { - return ( - !this.isNot === - urlMatches( - baseURL?.toLocaleLowerCase(), - lastCheckedURLString.toLocaleLowerCase(), - typeof expected === 'string' - ? expected.toLocaleLowerCase() - : expected, - ) - ); - } - - return ( - !this.isNot === - urlMatches( - baseURL, - lastCheckedURLString, - expected, - ) - ); - }, - { timeout }, - ); - - conditionSucceeded = true; - } catch (e) { - conditionSucceeded = false; - } - - if (conditionSucceeded) - return { pass: !this.isNot, message: () => '' }; - - return { - pass: this.isNot, - message: () => - textMatcherMessage( - this, - matcherName, - undefined, - expression, - expected, - lastCheckedURLString, - undefined, - 'string', - this.isNot, - true, - timeout, - ), - actual: lastCheckedURLString, - timeout, - }; + return toHaveURLExternal.call(this, page, expected, options); } export async function toBeOK( diff --git a/packages/playwright/src/matchers/toHaveURL.ts b/packages/playwright/src/matchers/toHaveURL.ts new file mode 100644 index 0000000000..ffdb740971 --- /dev/null +++ b/packages/playwright/src/matchers/toHaveURL.ts @@ -0,0 +1,152 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Page } from 'playwright-core'; +import type { ExpectMatcherState } from '../../types/test'; +import { EXPECTED_COLOR, printReceived } from '../common/expectBundle'; +import { matcherHint, type MatcherResult } from './matcherHint'; +import { urlMatches } from 'playwright-core/lib/utils'; +import { colors } from 'playwright-core/lib/utilsBundle'; +import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring } from './expect'; + +export async function toHaveURL( + this: ExpectMatcherState, + page: Page, + expected: string | RegExp | ((url: URL) => boolean), + options?: { ignoreCase?: boolean; timeout?: number }, +): Promise> { + const matcherName = 'toHaveURL'; + const expression = 'page'; + const matcherOptions = { + isNot: this.isNot, + promise: this.promise, + }; + + if ( + !(typeof expected === 'string') && + !(expected && 'test' in expected && typeof expected.test === 'function') && + !(typeof expected === 'function') + ) { + throw new Error( + [ + // Always display `expected` in expectation place + matcherHint(this, undefined, matcherName, expression, undefined, matcherOptions), + `${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected')} value must be a string, regular expression, or predicate`, + this.utils.printWithType('Expected', expected, this.utils.printExpected,), + ].join('\n\n'), + ); + } + + const timeout = options?.timeout ?? this.timeout; + let conditionSucceeded = false; + let lastCheckedURLString: string | undefined = undefined; + try { + await page.mainFrame().waitForURL( + url => { + const baseURL: string | undefined = (page.context() as any)._options + .baseURL; + lastCheckedURLString = url.toString(); + + if (options?.ignoreCase) { + return ( + !this.isNot === + urlMatches( + baseURL?.toLocaleLowerCase(), + lastCheckedURLString.toLocaleLowerCase(), + typeof expected === 'string' + ? expected.toLocaleLowerCase() + : expected, + ) + ); + } + + return ( + !this.isNot === urlMatches(baseURL, lastCheckedURLString, expected) + ); + }, + { timeout }, + ); + + conditionSucceeded = true; + } catch (e) { + conditionSucceeded = false; + } + + if (conditionSucceeded) + return { name: matcherName, pass: !this.isNot, message: () => '' }; + + return { + name: matcherName, + pass: this.isNot, + message: () => + toHaveURLMessage( + this, + matcherName, + expression, + expected, + lastCheckedURLString, + this.isNot, + true, + timeout, + ), + actual: lastCheckedURLString, + timeout, + }; +} + +function toHaveURLMessage( + state: ExpectMatcherState, + matcherName: string, + expression: string, + expected: string | RegExp | Function, + received: string | undefined, + pass: boolean, + didTimeout: boolean, + timeout: number, +): string { + const matcherOptions = { + isNot: state.isNot, + promise: state.promise, + }; + const receivedString = received || ''; + const messagePrefix = matcherHint(state, undefined, matcherName, expression, undefined, matcherOptions, didTimeout ? timeout : undefined); + + let printedReceived: string | undefined; + let printedExpected: string | undefined; + let printedDiff: string | undefined; + if (typeof expected === 'function') { + printedExpected = `Expected predicate to ${!state.isNot ? 'succeed' : 'fail'}`; + printedReceived = `Received string: ${printReceived(receivedString)}`; + } else { + if (pass) { + if (typeof expected === 'string') { + printedExpected = `Expected string: not ${state.utils.printExpected(expected)}`; + const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); + printedReceived = `Received string: ${formattedReceived}`; + } else { + printedExpected = `Expected pattern: not ${state.utils.printExpected(expected)}`; + const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); + printedReceived = `Received string: ${formattedReceived}`; + } + } else { + const labelExpected = `Expected ${typeof expected === 'string' ? 'string' : 'pattern'}`; + printedDiff = state.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false); + } + } + + const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived; + return messagePrefix + resultDetails; +} diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index b4ded21e0e..2f8bc34b21 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -15,11 +15,17 @@ */ -import { expectTypes } from '../util'; +import { expectTypes, callLogText } from '../util'; +import { + printReceivedStringContainExpectedResult, + printReceivedStringContainExpectedSubstring +} from './expect'; +import { EXPECTED_COLOR } from '../common/expectBundle'; import type { ExpectMatcherState } from '../../types/test'; +import { kNoElementsFoundError, matcherHint } from './matcherHint'; import type { MatcherResult } from './matcherHint'; import type { Locator } from 'playwright-core'; -import { textMatcherMessage, toMatchExpectedStringOrPredicateVerification } from './error'; +import { colors } from 'playwright-core/lib/utilsBundle'; export async function toMatchText( this: ExpectMatcherState, @@ -31,7 +37,23 @@ export async function toMatchText( options: { timeout?: number, matchSubstring?: boolean } = {}, ): Promise> { expectTypes(receiver, [receiverType], matcherName); - toMatchExpectedStringOrPredicateVerification(this, matcherName, receiver, receiver, expected); + + const matcherOptions = { + isNot: this.isNot, + promise: this.promise, + }; + + if ( + !(typeof expected === 'string') && + !(expected && typeof expected.test === 'function') + ) { + // Same format as jest's matcherErrorMessage + throw new Error([ + matcherHint(this, receiver, matcherName, receiver, expected, matcherOptions), + `${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected',)} value must be a string or regular expression`, + this.utils.printWithType('Expected', expected, this.utils.printExpected) + ].join('\n\n')); + } const timeout = options.timeout ?? this.timeout; @@ -46,24 +68,52 @@ export async function toMatchText( } const stringSubstring = options.matchSubstring ? 'substring' : 'string'; + const receivedString = received || ''; + const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); + const notFound = received === kNoElementsFoundError; + + let printedReceived: string | undefined; + let printedExpected: string | undefined; + let printedDiff: string | undefined; + if (pass) { + if (typeof expected === 'string') { + if (notFound) { + printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${received}`; + } else { + printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`; + const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); + printedReceived = `Received string: ${formattedReceived}`; + } + } else { + if (notFound) { + printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${received}`; + } else { + printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`; + const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); + printedReceived = `Received string: ${formattedReceived}`; + } + } + } else { + const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`; + if (notFound) { + printedExpected = `${labelExpected}: ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${received}`; + } else { + printedDiff = this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false); + } + } + + const message = () => { + const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived; + return messagePrefix + resultDetails + callLogText(log); + }; return { name: matcherName, expected, - message: () => - textMatcherMessage( - this, - matcherName, - receiver, - 'locator', - expected, - received, - log, - stringSubstring, - pass, - !!timedOut, - timeout, - ), + message, pass, actual: received, log,