chore: restore to.have.url matching via injected script (#35027)

This commit is contained in:
Adam Gastineau 2025-03-04 11:52:59 -08:00 committed by GitHub
parent 02a63fe9e8
commit d6a4c1cda4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 28 additions and 33 deletions

View File

@ -1423,7 +1423,6 @@ export class InjectedScript {
} else if (expression === 'to.have.title') { } else if (expression === 'to.have.title') {
received = this.document.title; received = this.document.title;
} else if (expression === 'to.have.url') { } else if (expression === 'to.have.url') {
// Note: this is used by all language ports except for javascript.
received = this.document.location.href; received = this.document.location.href;
} else if (expression === 'to.have.value') { } else if (expression === 'to.have.value') {
element = this.retarget(element, 'follow-label')!; element = this.retarget(element, 'follow-label')!;

View File

@ -14,13 +14,13 @@
* limitations under the License. * 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 { colors } from 'playwright-core/lib/utils';
import { callLogText, expectTypes } from '../util'; import { callLogText, expectTypes } from '../util';
import { toBeTruthy } from './toBeTruthy'; import { toBeTruthy } from './toBeTruthy';
import { toEqual } from './toEqual'; import { toEqual } from './toEqual';
import { toHaveURL as toHaveURLExternal } from './toHaveURL'; import { toHaveURLWithPredicate } from './toHaveURL';
import { toMatchText } from './toMatchText'; import { toMatchText } from './toMatchText';
import { takeFirst } from '../common/config'; import { takeFirst } from '../common/config';
import { currentTestInfo } from '../common/globals'; import { currentTestInfo } from '../common/globals';
@ -391,7 +391,17 @@ export function toHaveURL(
expected: string | RegExp | ((url: URL) => boolean), expected: string | RegExp | ((url: URL) => boolean),
options?: { ignoreCase?: boolean; timeout?: number }, 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( export async function toBeOK(

View File

@ -14,10 +14,10 @@
* limitations under the License. * 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 { colors } from 'playwright-core/lib/utils';
import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring } from './expect'; import { printReceivedStringContainExpectedResult } from './expect';
import { matcherHint } from './matcherHint'; import { matcherHint } from './matcherHint';
import { EXPECTED_COLOR, printReceived } from '../common/expectBundle'; import { EXPECTED_COLOR, printReceived } from '../common/expectBundle';
@ -25,10 +25,10 @@ import type { MatcherResult } from './matcherHint';
import type { ExpectMatcherState } from '../../types/test'; import type { ExpectMatcherState } from '../../types/test';
import type { Page } from 'playwright-core'; import type { Page } from 'playwright-core';
export async function toHaveURL( export async function toHaveURLWithPredicate(
this: ExpectMatcherState, this: ExpectMatcherState,
page: Page, page: Page,
expected: string | RegExp | ((url: URL) => boolean), expected: (url: URL) => boolean,
options?: { ignoreCase?: boolean; timeout?: number }, options?: { ignoreCase?: boolean; timeout?: number },
): Promise<MatcherResult<string | RegExp, string>> { ): Promise<MatcherResult<string | RegExp, string>> {
const matcherName = 'toHaveURL'; const matcherName = 'toHaveURL';
@ -38,11 +38,7 @@ export async function toHaveURL(
promise: this.promise, promise: this.promise,
}; };
if ( if (typeof expected !== 'function') {
!(typeof expected === 'string') &&
!(expected && 'test' in expected && typeof expected.test === 'function') &&
!(typeof expected === 'function')
) {
throw new Error( throw new Error(
[ [
// Always display `expected` in expectation place // Always display `expected` in expectation place
@ -68,9 +64,7 @@ export async function toHaveURL(
urlMatches( urlMatches(
baseURL?.toLocaleLowerCase(), baseURL?.toLocaleLowerCase(),
lastCheckedURLString.toLocaleLowerCase(), lastCheckedURLString.toLocaleLowerCase(),
typeof expected === 'string' expected,
? expected.toLocaleLowerCase()
: expected,
) )
); );
} }
@ -98,9 +92,7 @@ export async function toHaveURL(
this, this,
matcherName, matcherName,
expression, expression,
typeof expected === 'string' expected,
? constructURLBasedOnBaseURL(baseURL, expected)
: expected,
lastCheckedURLString, lastCheckedURLString,
this.isNot, this.isNot,
true, true,
@ -115,7 +107,7 @@ function toHaveURLMessage(
state: ExpectMatcherState, state: ExpectMatcherState,
matcherName: string, matcherName: string,
expression: string, expression: string,
expected: string | RegExp | Function, expected: Function,
received: string | undefined, received: string | undefined,
pass: boolean, pass: boolean,
didTimeout: boolean, didTimeout: boolean,
@ -136,15 +128,9 @@ function toHaveURLMessage(
printedReceived = `Received string: ${printReceived(receivedString)}`; printedReceived = `Received string: ${printReceived(receivedString)}`;
} else { } else {
if (pass) { if (pass) {
if (typeof expected === 'string') { printedExpected = `Expected pattern: not ${state.utils.printExpected(expected)}`;
printedExpected = `Expected string: not ${state.utils.printExpected(expected)}`; const formattedReceived = printReceivedStringContainExpectedResult(receivedString, null);
const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); printedReceived = `Received string: ${formattedReceived}`;
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 { } else {
const labelExpected = `Expected ${typeof expected === 'string' ? 'string' : 'pattern'}`; const labelExpected = `Expected ${typeof expected === 'string' ? 'string' : 'pattern'}`;
printedDiff = state.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false); printedDiff = state.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false);

View File

@ -244,7 +244,7 @@ test.describe('toHaveURL', () => {
test('fail string', async ({ page }) => { test('fail string', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>'); await page.goto('data:text/html,<div>A</div>');
const error = await expect(page).toHaveURL('wrong', { timeout: 1000 }).catch(e => e); 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,<div>A</div>"'); expect(stripVTControlCharacters(error.message)).toContain('Expected string: "wrong"\nReceived string: "data:text/html,<div>A</div>"');
}); });
@ -252,7 +252,7 @@ test.describe('toHaveURL', () => {
await page.goto('data:text/html,<div>A</div>'); await page.goto('data:text/html,<div>A</div>');
// @ts-expect-error // @ts-expect-error
const error = await expect(page).toHaveURL({}).catch(e => e); 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: {}'); expect(stripVTControlCharacters(error.message)).toContain('Expected has type: object\nExpected has value: {}');
}); });

View File

@ -548,7 +548,7 @@ test('should respect expect.timeout', async ({ runInlineTest }) => {
test('timeout', async ({ page }) => { test('timeout', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>'); await page.goto('data:text/html,<div>A</div>');
const error = await expect(page).toHaveURL('data:text/html,<div>B</div>').catch(e => e); const error = await expect(page).toHaveURL('data:text/html,<div>B</div>').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,<div>'); expect(error.message).toContain('data:text/html,<div>');
}); });
`, `,
@ -566,7 +566,7 @@ test('should support toHaveURL predicate', async ({ runInlineTest }) => {
test('predicate', async ({ page }) => { test('predicate', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>'); await page.goto('data:text/html,<div>A</div>');
const error = await expect(page).toHaveURL('data:text/html,<div>B</div>').catch(e => e); const error = await expect(page).toHaveURL(url => url === 'data:text/html,<div>B</div>').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(page).toHaveURL(expected)');
expect(error.message).toContain('data:text/html,<div>'); expect(error.message).toContain('data:text/html,<div>');
}); });