From afd19da215285ddac77d8a67c932241aa40cb40e Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 16 Jan 2025 12:24:14 -0800 Subject: [PATCH] Beginning of toHaveURL with predicate --- packages/playwright/src/matchers/error.ts | 50 +++++++++++++++++++ packages/playwright/src/matchers/expect.ts | 4 +- packages/playwright/src/matchers/matchers.ts | 50 +++++++++++++++++++ .../playwright/src/matchers/toMatchText.ts | 25 +++------- 4 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 packages/playwright/src/matchers/error.ts diff --git a/packages/playwright/src/matchers/error.ts b/packages/playwright/src/matchers/error.ts new file mode 100644 index 0000000000..13e01c4887 --- /dev/null +++ b/packages/playwright/src/matchers/error.ts @@ -0,0 +1,50 @@ +/** + * 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 { matcherHint } from './matcherHint'; +import { colors } from 'playwright-core/lib/utilsBundle'; +import type { Locator } from 'playwright-core'; +import { EXPECTED_COLOR } from '../common/expectBundle'; + +export function toMatchExpectedVerification( + 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([ + matcherHint(state, receiver, matcherName, expression, expected, matcherOptions), + `${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected',)} value must be a ${message}`, + state.utils.printWithType('Expected', expected, state.utils.printExpected) + ].join('\n\n')); + } +} diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index d382a4dbfd..903f816788 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -47,7 +47,8 @@ import { toHaveURL, toHaveValue, toHaveValues, - toPass + toPass, + toHaveURL2 } from './matchers'; import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot'; import type { Expect, ExpectMatcherState } from '../../types/test'; @@ -235,6 +236,7 @@ const customAsyncMatchers = { toHaveText, toHaveTitle, toHaveURL, + toHaveURL2, toHaveValue, toHaveValues, toHaveScreenshot, diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index c942fef246..cf79957949 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -26,6 +26,8 @@ import { currentTestInfo } from '../common/globals'; import { TestInfoImpl } from '../worker/testInfo'; import type { ExpectMatcherState } from '../../types/test'; import { takeFirst } from '../common/config'; +import { matcherHint } from './matcherHint'; +import { toMatchExpectedVerification } from './error'; export interface LocatorEx extends Locator { _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>; @@ -386,6 +388,54 @@ export function toHaveTitle( }, expected, options); } +export async function toHaveURL2( + this: ExpectMatcherState, + page: Page, + expected: string | RegExp | ((url: URL) => boolean), + options?: { ignoreCase?: boolean; timeout?: number }, +) { + const matcherName = 'toHaveURL'; + const expression = 'page'; + toMatchExpectedVerification( + this, + matcherName, + undefined, + expression, + expected, + true, + ); + + const timeout = options?.timeout ?? this.timeout; + let urlMatched = false; + try { + await page.mainFrame().waitForURL(expected, { timeout }); + urlMatched = true; + } catch (e) { + urlMatched = false; + } + + if (urlMatched !== this.isNot) + return { pass: urlMatched, message: () => '' }; + + const matcherOptions = { + isNot: this.isNot, + promise: this.promise, + }; + return { + pass: urlMatched, + message: () => + matcherHint( + this, + undefined, + matcherName, + expression, + typeof expected === 'function' ? 'predicate' : expected, + matcherOptions, + timeout, + ), + }; +} + export function toHaveURL( this: ExpectMatcherState, page: Page, diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 2f8bc34b21..650905163a 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -20,12 +20,11 @@ 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 { colors } from 'playwright-core/lib/utilsBundle'; +import { toMatchExpectedVerification } from './error'; export async function toMatchText( this: ExpectMatcherState, @@ -37,23 +36,7 @@ export async function toMatchText( options: { timeout?: number, matchSubstring?: boolean } = {}, ): Promise> { expectTypes(receiver, [receiverType], matcherName); - - 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')); - } + toMatchExpectedVerification(this, matcherName, receiver, receiver, expected); const timeout = options.timeout ?? this.timeout; @@ -67,6 +50,10 @@ export async function toMatchText( }; } + const matcherOptions = { + isNot: this.isNot, + promise: this.promise, + }; const stringSubstring = options.matchSubstring ? 'substring' : 'string'; const receivedString = received || ''; const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);