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"');
+});