Skip to content

Commit d6a4c1c

Browse files
authored
chore: restore to.have.url matching via injected script (#35027)
1 parent 02a63fe commit d6a4c1c

File tree

5 files changed

+28
-33
lines changed

5 files changed

+28
-33
lines changed

packages/playwright-core/src/server/injected/injectedScript.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1423,7 +1423,6 @@ export class InjectedScript {
14231423
} else if (expression === 'to.have.title') {
14241424
received = this.document.title;
14251425
} else if (expression === 'to.have.url') {
1426-
// Note: this is used by all language ports except for javascript.
14271426
received = this.document.location.href;
14281427
} else if (expression === 'to.have.value') {
14291428
element = this.retarget(element, 'follow-label')!;

packages/playwright/src/matchers/matchers.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils';
17+
import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils';
1818
import { colors } from 'playwright-core/lib/utils';
1919

2020
import { callLogText, expectTypes } from '../util';
2121
import { toBeTruthy } from './toBeTruthy';
2222
import { toEqual } from './toEqual';
23-
import { toHaveURL as toHaveURLExternal } from './toHaveURL';
23+
import { toHaveURLWithPredicate } from './toHaveURL';
2424
import { toMatchText } from './toMatchText';
2525
import { takeFirst } from '../common/config';
2626
import { currentTestInfo } from '../common/globals';
@@ -391,7 +391,17 @@ export function toHaveURL(
391391
expected: string | RegExp | ((url: URL) => boolean),
392392
options?: { ignoreCase?: boolean; timeout?: number },
393393
) {
394-
return toHaveURLExternal.call(this, page, expected, options);
394+
// Ports don't support predicates. Keep separate server and client codepaths
395+
if (typeof expected === 'function')
396+
return toHaveURLWithPredicate.call(this, page, expected, options);
397+
398+
const baseURL = (page.context() as any)._options.baseURL;
399+
expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected;
400+
const locator = page.locator(':root') as LocatorEx;
401+
return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout) => {
402+
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
403+
return await locator._expect('to.have.url', { expectedText, isNot, timeout });
404+
}, expected, options);
395405
}
396406

397407
export async function toBeOK(

packages/playwright/src/matchers/toHaveURL.ts

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { constructURLBasedOnBaseURL, urlMatches } from 'playwright-core/lib/utils';
17+
import { urlMatches } from 'playwright-core/lib/utils';
1818
import { colors } from 'playwright-core/lib/utils';
1919

20-
import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring } from './expect';
20+
import { printReceivedStringContainExpectedResult } from './expect';
2121
import { matcherHint } from './matcherHint';
2222
import { EXPECTED_COLOR, printReceived } from '../common/expectBundle';
2323

2424
import type { MatcherResult } from './matcherHint';
2525
import type { ExpectMatcherState } from '../../types/test';
2626
import type { Page } from 'playwright-core';
2727

28-
export async function toHaveURL(
28+
export async function toHaveURLWithPredicate(
2929
this: ExpectMatcherState,
3030
page: Page,
31-
expected: string | RegExp | ((url: URL) => boolean),
31+
expected: (url: URL) => boolean,
3232
options?: { ignoreCase?: boolean; timeout?: number },
3333
): Promise<MatcherResult<string | RegExp, string>> {
3434
const matcherName = 'toHaveURL';
@@ -38,11 +38,7 @@ export async function toHaveURL(
3838
promise: this.promise,
3939
};
4040

41-
if (
42-
!(typeof expected === 'string') &&
43-
!(expected && 'test' in expected && typeof expected.test === 'function') &&
44-
!(typeof expected === 'function')
45-
) {
41+
if (typeof expected !== 'function') {
4642
throw new Error(
4743
[
4844
// Always display `expected` in expectation place
@@ -68,9 +64,7 @@ export async function toHaveURL(
6864
urlMatches(
6965
baseURL?.toLocaleLowerCase(),
7066
lastCheckedURLString.toLocaleLowerCase(),
71-
typeof expected === 'string'
72-
? expected.toLocaleLowerCase()
73-
: expected,
67+
expected,
7468
)
7569
);
7670
}
@@ -98,9 +92,7 @@ export async function toHaveURL(
9892
this,
9993
matcherName,
10094
expression,
101-
typeof expected === 'string'
102-
? constructURLBasedOnBaseURL(baseURL, expected)
103-
: expected,
95+
expected,
10496
lastCheckedURLString,
10597
this.isNot,
10698
true,
@@ -115,7 +107,7 @@ function toHaveURLMessage(
115107
state: ExpectMatcherState,
116108
matcherName: string,
117109
expression: string,
118-
expected: string | RegExp | Function,
110+
expected: Function,
119111
received: string | undefined,
120112
pass: boolean,
121113
didTimeout: boolean,
@@ -136,15 +128,9 @@ function toHaveURLMessage(
136128
printedReceived = `Received string: ${printReceived(receivedString)}`;
137129
} else {
138130
if (pass) {
139-
if (typeof expected === 'string') {
140-
printedExpected = `Expected string: not ${state.utils.printExpected(expected)}`;
141-
const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length);
142-
printedReceived = `Received string: ${formattedReceived}`;
143-
} else {
144-
printedExpected = `Expected pattern: not ${state.utils.printExpected(expected)}`;
145-
const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null);
146-
printedReceived = `Received string: ${formattedReceived}`;
147-
}
131+
printedExpected = `Expected pattern: not ${state.utils.printExpected(expected)}`;
132+
const formattedReceived = printReceivedStringContainExpectedResult(receivedString, null);
133+
printedReceived = `Received string: ${formattedReceived}`;
148134
} else {
149135
const labelExpected = `Expected ${typeof expected === 'string' ? 'string' : 'pattern'}`;
150136
printedDiff = state.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false);

tests/page/expect-misc.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,15 +244,15 @@ test.describe('toHaveURL', () => {
244244
test('fail string', async ({ page }) => {
245245
await page.goto('data:text/html,<div>A</div>');
246246
const error = await expect(page).toHaveURL('wrong', { timeout: 1000 }).catch(e => e);
247-
expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(page).toHaveURL(expected)');
247+
expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(locator).toHaveURL(expected)');
248248
expect(stripVTControlCharacters(error.message)).toContain('Expected string: "wrong"\nReceived string: "data:text/html,<div>A</div>"');
249249
});
250250

251251
test('fail with invalid argument', async ({ page }) => {
252252
await page.goto('data:text/html,<div>A</div>');
253253
// @ts-expect-error
254254
const error = await expect(page).toHaveURL({}).catch(e => e);
255-
expect(stripVTControlCharacters(error.message)).toContain('expect(page).toHaveURL(expected)\n\n\n\nMatcher error: expected value must be a string, regular expression, or predicate');
255+
expect(stripVTControlCharacters(error.message)).toContain(`expect(locator(':root')).toHaveURL([object Object])`);
256256
expect(stripVTControlCharacters(error.message)).toContain('Expected has type: object\nExpected has value: {}');
257257
});
258258

tests/playwright-test/expect.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ test('should respect expect.timeout', async ({ runInlineTest }) => {
548548
test('timeout', async ({ page }) => {
549549
await page.goto('data:text/html,<div>A</div>');
550550
const error = await expect(page).toHaveURL('data:text/html,<div>B</div>').catch(e => e);
551-
expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(page).toHaveURL(expected)');
551+
expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(locator).toHaveURL(expected)');
552552
expect(error.message).toContain('data:text/html,<div>');
553553
});
554554
`,
@@ -566,7 +566,7 @@ test('should support toHaveURL predicate', async ({ runInlineTest }) => {
566566
567567
test('predicate', async ({ page }) => {
568568
await page.goto('data:text/html,<div>A</div>');
569-
const error = await expect(page).toHaveURL('data:text/html,<div>B</div>').catch(e => e);
569+
const error = await expect(page).toHaveURL(url => url === 'data:text/html,<div>B</div>').catch(e => e);
570570
expect(stripVTControlCharacters(error.message)).toContain('Timed out 1000ms waiting for expect(page).toHaveURL(expected)');
571571
expect(error.message).toContain('data:text/html,<div>');
572572
});

0 commit comments

Comments
 (0)