From f583f1604cc1d3e2f1e5b40c60d1ff061e58a01b Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 3 Dec 2021 09:27:06 -0800 Subject: [PATCH] feat(locator.withText): allow narrowing locators to those with text (#10688) --- docs/src/api/class-locator.md | 12 +++++++++ docs/src/api/class-locatorassertions.md | 1 - .../playwright-core/src/client/locator.ts | 9 ++++++- .../src/server/supplements/recorder/csharp.ts | 3 ++- .../src/server/supplements/recorder/java.ts | 3 ++- .../server/supplements/recorder/javascript.ts | 3 ++- .../src/server/supplements/recorder/python.ts | 3 ++- .../src/server/supplements/recorder/utils.ts | 12 --------- .../playwright-core/src/utils/stringUtils.ts | 27 +++++++++++++++++++ packages/playwright-core/types/types.d.ts | 9 ++++++- tests/page/locator-query.spec.ts | 25 +++++++++++++++++ 11 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 packages/playwright-core/src/utils/stringUtils.ts diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 6153769306..921086c11a 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -1148,3 +1148,15 @@ orderSent.WaitForAsync(); ### option: Locator.waitFor.state = %%-wait-for-selector-state-%% ### option: Locator.waitFor.timeout = %%-input-timeout-%% + +## method: Locator.withText +- returns: <[Locator]> + +Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example, `"Playwright"` +matches `
Playwright
`. + + +### param: Locator.withText.text +- `text` <[string]|[RegExp]> + +Text to filter by as a string or as a regular expression. diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 6a07c061a3..9e0087d229 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -281,7 +281,6 @@ expect(locator).to_be_checked() ``` ### option: LocatorAssertions.toBeChecked.checked -* langs: js - `checked` <[boolean]> ### option: LocatorAssertions.toBeChecked.timeout = %%-assertions-timeout-%% diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 7456549931..f06897a31e 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -18,11 +18,12 @@ import * as structs from '../../types/structs'; import * as api from '../../types/types'; import * as channels from '../protocol/channels'; import * as util from 'util'; -import { monotonicTime } from '../utils/utils'; +import { isRegExp, monotonicTime } from '../utils/utils'; import { ElementHandle } from './elementHandle'; import { Frame } from './frame'; import { FilePayload, FrameExpectOptions, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types'; import { parseResult, serializeArgument } from './jsHandle'; +import { escapeWithQuotes } from '../utils/stringUtils'; export class Locator implements api.Locator { private _frame: Frame; @@ -97,6 +98,12 @@ export class Locator implements api.Locator { return new Locator(this._frame, this._selector + ' >> ' + selector); } + withText(text: string | RegExp): Locator { + const matcher = isRegExp(text) ? 'text-matches' : 'has-text'; + const source = escapeWithQuotes(isRegExp(text) ? text.source : text, '"'); + return new Locator(this._frame, this._selector + ` >> :scope:${matcher}(${source})`); + } + frameLocator(selector: string): FrameLocator { return new FrameLocator(this._frame, this._selector + ' >> ' + selector); } diff --git a/packages/playwright-core/src/server/supplements/recorder/csharp.ts b/packages/playwright-core/src/server/supplements/recorder/csharp.ts index fc9c0e97ff..58cbbbd547 100644 --- a/packages/playwright-core/src/server/supplements/recorder/csharp.ts +++ b/packages/playwright-core/src/server/supplements/recorder/csharp.ts @@ -18,7 +18,8 @@ import type { BrowserContextOptions } from '../../../..'; import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language'; import { ActionInContext } from './codeGenerator'; import { actionTitle, Action } from './recorderActions'; -import { escapeWithQuotes, MouseClickOptions, toModifiers } from './utils'; +import { MouseClickOptions, toModifiers } from './utils'; +import { escapeWithQuotes } from '../../../utils/stringUtils'; import deviceDescriptors from '../../deviceDescriptors'; export class CSharpLanguageGenerator implements LanguageGenerator { diff --git a/packages/playwright-core/src/server/supplements/recorder/java.ts b/packages/playwright-core/src/server/supplements/recorder/java.ts index 5313ec99e2..31e545976e 100644 --- a/packages/playwright-core/src/server/supplements/recorder/java.ts +++ b/packages/playwright-core/src/server/supplements/recorder/java.ts @@ -18,9 +18,10 @@ import type { BrowserContextOptions } from '../../../..'; import { LanguageGenerator, LanguageGeneratorOptions, toSignalMap } from './language'; import { ActionInContext } from './codeGenerator'; import { Action, actionTitle } from './recorderActions'; -import { escapeWithQuotes, MouseClickOptions, toModifiers } from './utils'; +import { MouseClickOptions, toModifiers } from './utils'; import deviceDescriptors from '../../deviceDescriptors'; import { JavaScriptFormatter } from './javascript'; +import { escapeWithQuotes } from '../../../utils/stringUtils'; export class JavaLanguageGenerator implements LanguageGenerator { id = 'java'; diff --git a/packages/playwright-core/src/server/supplements/recorder/javascript.ts b/packages/playwright-core/src/server/supplements/recorder/javascript.ts index f6ceb70ccd..4722180eb7 100644 --- a/packages/playwright-core/src/server/supplements/recorder/javascript.ts +++ b/packages/playwright-core/src/server/supplements/recorder/javascript.ts @@ -18,8 +18,9 @@ import type { BrowserContextOptions } from '../../../..'; import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language'; import { ActionInContext } from './codeGenerator'; import { Action, actionTitle } from './recorderActions'; -import { escapeWithQuotes, MouseClickOptions, toModifiers } from './utils'; +import { MouseClickOptions, toModifiers } from './utils'; import deviceDescriptors from '../../deviceDescriptors'; +import { escapeWithQuotes } from '../../../utils/stringUtils'; export class JavaScriptLanguageGenerator implements LanguageGenerator { id: string; diff --git a/packages/playwright-core/src/server/supplements/recorder/python.ts b/packages/playwright-core/src/server/supplements/recorder/python.ts index 7bc4ce5378..1247657b85 100644 --- a/packages/playwright-core/src/server/supplements/recorder/python.ts +++ b/packages/playwright-core/src/server/supplements/recorder/python.ts @@ -18,7 +18,8 @@ import type { BrowserContextOptions } from '../../../..'; import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language'; import { ActionInContext } from './codeGenerator'; import { actionTitle, Action } from './recorderActions'; -import { escapeWithQuotes, MouseClickOptions, toModifiers } from './utils'; +import { MouseClickOptions, toModifiers } from './utils'; +import { escapeWithQuotes } from '../../../utils/stringUtils'; import deviceDescriptors from '../../deviceDescriptors'; export class PythonLanguageGenerator implements LanguageGenerator { diff --git a/packages/playwright-core/src/server/supplements/recorder/utils.ts b/packages/playwright-core/src/server/supplements/recorder/utils.ts index 247ec22a49..4dea686ee7 100644 --- a/packages/playwright-core/src/server/supplements/recorder/utils.ts +++ b/packages/playwright-core/src/server/supplements/recorder/utils.ts @@ -58,15 +58,3 @@ export function describeFrame(frame: Frame): { frameName?: string, frameUrl: str return { isMainFrame: false, frameUrl: frame.url(), frameName: frame.name() }; return { isMainFrame: false, frameUrl: frame.url() }; } - -export function escapeWithQuotes(text: string, char: string = '\'') { - const stringified = JSON.stringify(text); - const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"'); - if (char === '\'') - return char + escapedText.replace(/[']/g, '\\\'') + char; - if (char === '"') - return char + escapedText.replace(/["]/g, '\\"') + char; - if (char === '`') - return char + escapedText.replace(/[`]/g, '`') + char; - throw new Error('Invalid escape char'); -} diff --git a/packages/playwright-core/src/utils/stringUtils.ts b/packages/playwright-core/src/utils/stringUtils.ts new file mode 100644 index 0000000000..5c1272de47 --- /dev/null +++ b/packages/playwright-core/src/utils/stringUtils.ts @@ -0,0 +1,27 @@ +/** + * 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. + */ + +export function escapeWithQuotes(text: string, char: string = '\'') { + const stringified = JSON.stringify(text); + const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"'); + if (char === '\'') + return char + escapedText.replace(/[']/g, '\\\'') + char; + if (char === '"') + return char + escapedText.replace(/["]/g, '\\"') + char; + if (char === '`') + return char + escapedText.replace(/[`]/g, '`') + char; + throw new Error('Invalid escape char'); +} diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 5d4214f528..af43637aba 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9830,7 +9830,14 @@ export interface Locator { * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. */ timeout?: number; - }): Promise;} + }): Promise; + + /** + * Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example, + * `"Playwright"` matches `
Playwright
`. + * @param text Text to filter by as a string or as a regular expression. + */ + withText(text: string|RegExp): Locator;} /** * BrowserType provides methods to launch a specific browser instance or connect to an existing one. The following is a diff --git a/tests/page/locator-query.spec.ts b/tests/page/locator-query.spec.ts index 206647df99..fb775fc27e 100644 --- a/tests/page/locator-query.spec.ts +++ b/tests/page/locator-query.spec.ts @@ -59,3 +59,28 @@ it('should throw on due to strictness 2', async ({ page }) => { const e = await page.locator('option').evaluate(e => {}).catch(e => e); expect(e.message).toContain(`strict mode violation`); }); + +it('should filter by text', async ({ page }) => { + await page.setContent(`
Foobar
Bar
`); + await expect(page.locator('div').withText('Foo')).toHaveText('Foobar'); +}); + +it('should filter by text 2', async ({ page }) => { + await page.setContent(`
foo hello world bar
`); + await expect(page.locator('div').withText('hello world')).toHaveText('foo hello world bar'); +}); + +it('should filter by regex', async ({ page }) => { + await page.setContent(`
Foobar
Bar
`); + await expect(page.locator('div').withText(/Foo.*/)).toHaveText('Foobar'); +}); + +it('should filter by text with quotes', async ({ page }) => { + await page.setContent(`
Hello "world"
Hello world
`); + await expect(page.locator('div').withText('Hello "world"')).toHaveText('Hello "world"'); +}); + +it('should filter by regex with quotes', async ({ page }) => { + await page.setContent(`
Hello "world"
Hello world
`); + await expect(page.locator('div').withText(/Hello "world"/)).toHaveText('Hello "world"'); +});