feat(locator.withText): allow narrowing locators to those with text (#10688)
This commit is contained in:
parent
b9aad6ef49
commit
f583f1604c
|
|
@ -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 `<article><div>Playwright</div></article>`.
|
||||
|
||||
|
||||
### param: Locator.withText.text
|
||||
- `text` <[string]|[RegExp]>
|
||||
|
||||
Text to filter by as a string or as a regular expression.
|
||||
|
|
|
|||
|
|
@ -281,7 +281,6 @@ expect(locator).to_be_checked()
|
|||
```
|
||||
|
||||
### option: LocatorAssertions.toBeChecked.checked
|
||||
* langs: js
|
||||
- `checked` <[boolean]>
|
||||
|
||||
### option: LocatorAssertions.toBeChecked.timeout = %%-assertions-timeout-%%
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
27
packages/playwright-core/src/utils/stringUtils.ts
Normal file
27
packages/playwright-core/src/utils/stringUtils.ts
Normal file
|
|
@ -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');
|
||||
}
|
||||
9
packages/playwright-core/types/types.d.ts
vendored
9
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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<void>;}
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example,
|
||||
* `"Playwright"` matches `<article><div>Playwright</div></article>`.
|
||||
* @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
|
||||
|
|
|
|||
|
|
@ -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(`<div>Foobar</div><div>Bar</div>`);
|
||||
await expect(page.locator('div').withText('Foo')).toHaveText('Foobar');
|
||||
});
|
||||
|
||||
it('should filter by text 2', async ({ page }) => {
|
||||
await page.setContent(`<div>foo <span>hello world</span> bar</div>`);
|
||||
await expect(page.locator('div').withText('hello world')).toHaveText('foo hello world bar');
|
||||
});
|
||||
|
||||
it('should filter by regex', async ({ page }) => {
|
||||
await page.setContent(`<div>Foobar</div><div>Bar</div>`);
|
||||
await expect(page.locator('div').withText(/Foo.*/)).toHaveText('Foobar');
|
||||
});
|
||||
|
||||
it('should filter by text with quotes', async ({ page }) => {
|
||||
await page.setContent(`<div>Hello "world"</div><div>Hello world</div>`);
|
||||
await expect(page.locator('div').withText('Hello "world"')).toHaveText('Hello "world"');
|
||||
});
|
||||
|
||||
it('should filter by regex with quotes', async ({ page }) => {
|
||||
await page.setContent(`<div>Hello "world"</div><div>Hello world</div>`);
|
||||
await expect(page.locator('div').withText(/Hello "world"/)).toHaveText('Hello "world"');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue