diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 2ae2f3970e20a..70374852f7ffc 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -1423,7 +1423,6 @@ export class InjectedScript { } else if (expression === 'to.have.title') { received = this.document.title; } else if (expression === 'to.have.url') { - // Note: this is used by all language ports except for javascript. received = this.document.location.href; } else if (expression === 'to.have.value') { element = this.retarget(element, 'follow-label')!; diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index d37e3285f455e..2992a41918c2a 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils'; +import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils'; import { colors } from 'playwright-core/lib/utils'; import { callLogText, expectTypes } from '../util'; import { toBeTruthy } from './toBeTruthy'; import { toEqual } from './toEqual'; -import { toHaveURL as toHaveURLExternal } from './toHaveURL'; +import { toHaveURLWithPredicate } from './toHaveURL'; import { toMatchText } from './toMatchText'; import { takeFirst } from '../common/config'; import { currentTestInfo } from '../common/globals'; @@ -391,7 +391,17 @@ export function toHaveURL( expected: string | RegExp | ((url: URL) => boolean), options?: { ignoreCase?: boolean; timeout?: number }, ) { - return toHaveURLExternal.call(this, page, expected, options); + // Ports don't support predicates. Keep separate server and client codepaths + if (typeof expected === 'function') + return toHaveURLWithPredicate.call(this, page, expected, options); + + const baseURL = (page.context() as any)._options.baseURL; + expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected; + const locator = page.locator(':root') as LocatorEx; + return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout) => { + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); + return await locator._expect('to.have.url', { expectedText, isNot, timeout }); + }, expected, options); } export async function toBeOK( diff --git a/packages/playwright/src/matchers/toHaveURL.ts b/packages/playwright/src/matchers/toHaveURL.ts index efc0ebd5f2fce..34f05fac33be2 100644 --- a/packages/playwright/src/matchers/toHaveURL.ts +++ b/packages/playwright/src/matchers/toHaveURL.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { constructURLBasedOnBaseURL, urlMatches } from 'playwright-core/lib/utils'; +import { urlMatches } from 'playwright-core/lib/utils'; import { colors } from 'playwright-core/lib/utils'; -import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring } from './expect'; +import { printReceivedStringContainExpectedResult } from './expect'; import { matcherHint } from './matcherHint'; import { EXPECTED_COLOR, printReceived } from '../common/expectBundle'; @@ -25,10 +25,10 @@ import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; import type { Page } from 'playwright-core'; -export async function toHaveURL( +export async function toHaveURLWithPredicate( this: ExpectMatcherState, page: Page, - expected: string | RegExp | ((url: URL) => boolean), + expected: (url: URL) => boolean, options?: { ignoreCase?: boolean; timeout?: number }, ): Promise> { const matcherName = 'toHaveURL'; @@ -38,11 +38,7 @@ export async function toHaveURL( promise: this.promise, }; - if ( - !(typeof expected === 'string') && - !(expected && 'test' in expected && typeof expected.test === 'function') && - !(typeof expected === 'function') - ) { + if (typeof expected !== 'function') { throw new Error( [ // Always display `expected` in expectation place @@ -68,9 +64,7 @@ export async function toHaveURL( urlMatches( baseURL?.toLocaleLowerCase(), lastCheckedURLString.toLocaleLowerCase(), - typeof expected === 'string' - ? expected.toLocaleLowerCase() - : expected, + expected, ) ); } @@ -98,9 +92,7 @@ export async function toHaveURL( this, matcherName, expression, - typeof expected === 'string' - ? constructURLBasedOnBaseURL(baseURL, expected) - : expected, + expected, lastCheckedURLString, this.isNot, true, @@ -115,7 +107,7 @@ function toHaveURLMessage( state: ExpectMatcherState, matcherName: string, expression: string, - expected: string | RegExp | Function, + expected: Function, received: string | undefined, pass: boolean, didTimeout: boolean, @@ -136,15 +128,9 @@ function toHaveURLMessage( 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}`; - } + printedExpected = `Expected pattern: not ${state.utils.printExpected(expected)}`; + const formattedReceived = printReceivedStringContainExpectedResult(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); diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index aca3c6d3dd628..f54c8271c36e5 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -244,7 +244,7 @@ test.describe('toHaveURL', () => { test('fail string', async ({ page }) => { await page.goto('data:text/html,
A
'); const error = await expect(page).toHaveURL('wrong', { timeout: 1000 }).catch(e => e); - expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(page).toHaveURL(expected)'); + expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(locator).toHaveURL(expected)'); expect(stripVTControlCharacters(error.message)).toContain('Expected string: "wrong"\nReceived string: "data:text/html,
A
"'); }); @@ -252,7 +252,7 @@ test.describe('toHaveURL', () => { await page.goto('data:text/html,
A
'); // @ts-expect-error const error = await expect(page).toHaveURL({}).catch(e => e); - expect(stripVTControlCharacters(error.message)).toContain('expect(page).toHaveURL(expected)\n\n\n\nMatcher error: expected value must be a string, regular expression, or predicate'); + expect(stripVTControlCharacters(error.message)).toContain(`expect(locator(':root')).toHaveURL([object Object])`); expect(stripVTControlCharacters(error.message)).toContain('Expected has type: object\nExpected has value: {}'); }); diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index 9e47f5282f27b..2563e97b4ae75 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -548,7 +548,7 @@ test('should respect expect.timeout', async ({ runInlineTest }) => { test('timeout', async ({ page }) => { await page.goto('data:text/html,
A
'); const error = await expect(page).toHaveURL('data:text/html,
B
').catch(e => e); - expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(page).toHaveURL(expected)'); + expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(locator).toHaveURL(expected)'); expect(error.message).toContain('data:text/html,
'); }); `, @@ -566,7 +566,7 @@ test('should support toHaveURL predicate', async ({ runInlineTest }) => { test('predicate', async ({ page }) => { await page.goto('data:text/html,
A
'); - const error = await expect(page).toHaveURL('data:text/html,
B
').catch(e => e); + const error = await expect(page).toHaveURL(url => url === 'data:text/html,
B
').catch(e => e); expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(page).toHaveURL(expected)'); expect(error.message).toContain('data:text/html,
'); });