feat(locator.withText): allow narrowing locators to those with text (#10688)

This commit is contained in:
Pavel Feldman 2021-12-03 09:27:06 -08:00 committed by GitHub
parent b9aad6ef49
commit f583f1604c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 88 additions and 19 deletions

View file

@ -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.

View file

@ -281,7 +281,6 @@ expect(locator).to_be_checked()
```
### option: LocatorAssertions.toBeChecked.checked
* langs: js
- `checked` <[boolean]>
### option: LocatorAssertions.toBeChecked.timeout = %%-assertions-timeout-%%

View file

@ -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);
}

View file

@ -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 {

View file

@ -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';

View file

@ -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;

View file

@ -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 {

View file

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

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

View file

@ -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

View file

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