mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-03 07:51:12 +03:00
feat(locator): filter({ hasNotText }) (#22222)
The opposite of `filter({ hasText })`.
This commit is contained in:
parent
29643a7bff
commit
35afb056ea
@ -1348,6 +1348,9 @@ Returns whether the element is [visible](../actionability.md#visible). [`option:
|
||||
### option: Frame.locator.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
### option: Frame.locator.hasNotText = %%-locator-option-has-not-text-%%
|
||||
* since: v1.33
|
||||
|
||||
## method: Frame.name
|
||||
* since: v1.8
|
||||
- returns: <[string]>
|
||||
|
@ -205,6 +205,9 @@ Returns locator to the last matching frame.
|
||||
### option: FrameLocator.locator.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
### option: FrameLocator.locator.hasNotText = %%-locator-option-has-not-text-%%
|
||||
* since: v1.33
|
||||
|
||||
## method: FrameLocator.nth
|
||||
* since: v1.17
|
||||
- returns: <[FrameLocator]>
|
||||
|
@ -991,6 +991,9 @@ await rowLocator
|
||||
### option: Locator.filter.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
### option: Locator.filter.hasNotText = %%-locator-option-has-not-text-%%
|
||||
* since: v1.33
|
||||
|
||||
## method: Locator.first
|
||||
* since: v1.14
|
||||
- returns: <[Locator]>
|
||||
@ -1508,6 +1511,8 @@ var banana = await page.GetByRole(AriaRole.Listitem).Last(1);
|
||||
### option: Locator.locator.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
### option: Locator.locator.hasNotText = %%-locator-option-has-not-text-%%
|
||||
* since: v1.33
|
||||
|
||||
## method: Locator.not
|
||||
* since: v1.33
|
||||
|
@ -2687,6 +2687,9 @@ Returns whether the element is [visible](../actionability.md#visible). [`option:
|
||||
### option: Page.locator.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
### option: Page.locator.hasNotText = %%-locator-option-has-not-text-%%
|
||||
* since: v1.33
|
||||
|
||||
## method: Page.mainFrame
|
||||
* since: v1.8
|
||||
- returns: <[Frame]>
|
||||
|
@ -1037,6 +1037,11 @@ For example, `article` that does not have `div` matches `<article><span>Playwrig
|
||||
|
||||
Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s.
|
||||
|
||||
## locator-option-has-not-text
|
||||
- `hasNotText` <[string]|[RegExp]>
|
||||
|
||||
Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element. When passed a [string], matching is case-insensitive and searches for a substring.
|
||||
|
||||
## locator-options-list-v1.14
|
||||
- %%-locator-option-has-text-%%
|
||||
- %%-locator-option-has-%%
|
||||
|
@ -883,6 +883,36 @@ await page
|
||||
.ClickAsync();
|
||||
```
|
||||
|
||||
Alternatively, filter by **not having** text:
|
||||
|
||||
```js
|
||||
// 5 in-stock items
|
||||
await expect(page.getByRole('listitem').filter({ hasNotText: 'Out of stock' })).toHaveCount(5);
|
||||
```
|
||||
|
||||
```java
|
||||
// 5 in-stock items
|
||||
assertThat(page.getByRole(AriaRole.LISTITEM)
|
||||
.filter(new Locator.FilterOptions().setHasNotText("Out of stock")))
|
||||
.hasCount(5);
|
||||
```
|
||||
|
||||
```python async
|
||||
# 5 in-stock items
|
||||
await expect(page.get_by_role("listitem").filter(has_not_text="Out of stock")).to_have_count(5)
|
||||
```
|
||||
|
||||
```python sync
|
||||
# 5 in-stock items
|
||||
expect(page.get_by_role("listitem").filter(has_not_text="Out of stock")).to_have_count(5)
|
||||
```
|
||||
|
||||
```csharp
|
||||
// 5 in-stock items
|
||||
await Expect(page.getByRole(AriaRole.Listitem).Filter(new() { HasNotText = "Out of stock" }))
|
||||
.ToHaveCountAsync(5);
|
||||
```
|
||||
|
||||
### Filter by child/descendant
|
||||
|
||||
Locators support an option to only select elements that have or have not a descendant matching another locator. You can therefore filter by any other locator such as a [`method: Locator.getByRole`], [`method: Locator.getByTestId`], [`method: Locator.getByText`] etc.
|
||||
|
@ -29,6 +29,7 @@ import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, get
|
||||
|
||||
export type LocatorOptions = {
|
||||
hasText?: string | RegExp;
|
||||
hasNotText?: string | RegExp;
|
||||
has?: Locator;
|
||||
hasNot?: Locator;
|
||||
};
|
||||
@ -44,6 +45,9 @@ export class Locator implements api.Locator {
|
||||
if (options?.hasText)
|
||||
this._selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
|
||||
|
||||
if (options?.hasNotText)
|
||||
this._selector += ` >> internal:has-not-text=${escapeForTextSelector(options.hasNotText, false)}`;
|
||||
|
||||
if (options?.has) {
|
||||
const locator = options.has;
|
||||
if (locator._frame !== frame)
|
||||
|
@ -29,11 +29,13 @@ class Locator {
|
||||
element: Element | undefined;
|
||||
elements: Element[] | undefined;
|
||||
|
||||
constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, has?: Locator, hasNot?: Locator }) {
|
||||
constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, hasNotText?: string | RegExp, has?: Locator, hasNot?: Locator }) {
|
||||
(this as any)[selectorSymbol] = selector;
|
||||
(this as any)[injectedScriptSymbol] = injectedScript;
|
||||
if (options?.hasText)
|
||||
selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
|
||||
if (options?.hasNotText)
|
||||
selector += ` >> internal:has-not-text=${escapeForTextSelector(options.hasNotText, false)}`;
|
||||
if (options?.has)
|
||||
selector += ` >> internal:has=` + JSON.stringify((options.has as any)[selectorSymbol]);
|
||||
if (options?.hasNot)
|
||||
|
@ -119,6 +119,7 @@ export class InjectedScript {
|
||||
this._engines.set('internal:label', this._createInternalLabelEngine());
|
||||
this._engines.set('internal:text', this._createTextEngine(true, true));
|
||||
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
|
||||
this._engines.set('internal:has-not-text', this._createInternalHasNotTextEngine());
|
||||
this._engines.set('internal:attr', this._createNamedAttributeEngine());
|
||||
this._engines.set('internal:testid', this._createNamedAttributeEngine());
|
||||
this._engines.set('internal:role', createRoleEngine(true));
|
||||
@ -309,6 +310,19 @@ export class InjectedScript {
|
||||
};
|
||||
}
|
||||
|
||||
private _createInternalHasNotTextEngine(): SelectorEngine {
|
||||
return {
|
||||
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||
return [];
|
||||
const element = root as Element;
|
||||
const text = elementText(this._evaluator._cacheText, element);
|
||||
const { matcher } = createTextMatcher(selector, true);
|
||||
return matcher(text) ? [] : [element];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _createInternalLabelEngine(): SelectorEngine {
|
||||
return {
|
||||
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
||||
|
@ -35,7 +35,9 @@ export class Selectors {
|
||||
'data-testid', 'data-testid:light',
|
||||
'data-test-id', 'data-test-id:light',
|
||||
'data-test', 'data-test:light',
|
||||
'nth', 'visible', 'internal:control', 'internal:has', 'internal:has-not', 'internal:has-text',
|
||||
'nth', 'visible', 'internal:control',
|
||||
'internal:has', 'internal:has-not',
|
||||
'internal:has-text', 'internal:has-not-text',
|
||||
'internal:or', 'internal:and', 'internal:not',
|
||||
'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role', 'internal:testid',
|
||||
]);
|
||||
|
@ -19,7 +19,7 @@ import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringi
|
||||
import type { ParsedSelector } from './selectorParser';
|
||||
|
||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
||||
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has' | 'hasNot' | 'frame' | 'or' | 'and' | 'not';
|
||||
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'or' | 'and' | 'not';
|
||||
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||
|
||||
type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp };
|
||||
@ -81,6 +81,14 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (part.name === 'internal:has-not-text') {
|
||||
const { exact, text } = detectExact(part.body as string);
|
||||
// There is no locator equivalent for strict has-not-text, leave it as is.
|
||||
if (!exact) {
|
||||
tokens.push(factory.generateLocator(base, 'has-not-text', text, { exact }));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (part.name === 'internal:has') {
|
||||
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||
tokens.push(factory.generateLocator(base, 'has', inner));
|
||||
@ -213,6 +221,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||
return `getByRole(${this.quote(body as string)}${attrString})`;
|
||||
case 'has-text':
|
||||
return `filter({ hasText: ${this.toHasText(body as string)} })`;
|
||||
case 'has-not-text':
|
||||
return `filter({ hasNotText: ${this.toHasText(body as string)} })`;
|
||||
case 'has':
|
||||
return `filter({ has: ${body} })`;
|
||||
case 'hasNot':
|
||||
@ -289,6 +299,8 @@ export class PythonLocatorFactory implements LocatorFactory {
|
||||
return `get_by_role(${this.quote(body as string)}${attrString})`;
|
||||
case 'has-text':
|
||||
return `filter(has_text=${this.toHasText(body as string)})`;
|
||||
case 'has-not-text':
|
||||
return `filter(has_not_text=${this.toHasText(body as string)})`;
|
||||
case 'has':
|
||||
return `filter(has=${body})`;
|
||||
case 'hasNot':
|
||||
@ -374,6 +386,8 @@ export class JavaLocatorFactory implements LocatorFactory {
|
||||
return `getByRole(AriaRole.${toSnakeCase(body as string).toUpperCase()}${attrString})`;
|
||||
case 'has-text':
|
||||
return `filter(new ${clazz}.FilterOptions().setHasText(${this.toHasText(body)}))`;
|
||||
case 'has-not-text':
|
||||
return `filter(new ${clazz}.FilterOptions().setHasNotText(${this.toHasText(body)}))`;
|
||||
case 'has':
|
||||
return `filter(new ${clazz}.FilterOptions().setHas(${body}))`;
|
||||
case 'hasNot':
|
||||
@ -453,6 +467,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||
return `GetByRole(AriaRole.${toTitleCase(body as string)}${attrString})`;
|
||||
case 'has-text':
|
||||
return `Filter(new() { ${this.toHasText(body)} })`;
|
||||
case 'has-not-text':
|
||||
return `Filter(new() { ${this.toHasNotText(body)} })`;
|
||||
case 'has':
|
||||
return `Filter(new() { Has = ${body} })`;
|
||||
case 'hasNot':
|
||||
@ -499,6 +515,12 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||
return `HasText = ${this.quote(body)}`;
|
||||
}
|
||||
|
||||
private toHasNotText(body: string | RegExp) {
|
||||
if (isRegExp(body))
|
||||
return `HasNotTextRegex = ${this.regexToString(body)}`;
|
||||
return `HasNotText = ${this.quote(body)}`;
|
||||
}
|
||||
|
||||
private quote(text: string) {
|
||||
return escapeWithQuotes(text, '\"');
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ function parseLocator(locator: string, testIdAttributeName: string): string {
|
||||
.replace(/get_by_alt_text/g, 'getbyalttext')
|
||||
.replace(/get_by_test_id/g, 'getbytestid')
|
||||
.replace(/get_by_([\w]+)/g, 'getby$1')
|
||||
.replace(/has_not_text/g, 'hasnottext')
|
||||
.replace(/has_text/g, 'hastext')
|
||||
.replace(/has_not/g, 'hasnot')
|
||||
.replace(/frame_locator/g, 'framelocator')
|
||||
@ -152,6 +153,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
||||
.replace(/last(\(\))?/g, 'nth=-1')
|
||||
.replace(/nth\(([^)]+)\)/g, 'nth=$1')
|
||||
.replace(/filter\(,?hastext=([^)]+)\)/g, 'internal:has-text=$1')
|
||||
.replace(/filter\(,?hasnottext=([^)]+)\)/g, 'internal:has-not-text=$1')
|
||||
.replace(/filter\(,?has2=([^)]+)\)/g, 'internal:has=$1')
|
||||
.replace(/filter\(,?hasnot2=([^)]+)\)/g, 'internal:has-not=$1')
|
||||
.replace(/,exact=false/g, '')
|
||||
|
30
packages/playwright-core/types/types.d.ts
vendored
30
packages/playwright-core/types/types.d.ts
vendored
@ -3225,6 +3225,12 @@ export interface Page {
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
|
||||
* When passed a [string], matching is case-insensitive and searches for a substring.
|
||||
*/
|
||||
hasNotText?: string|RegExp;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
@ -6610,6 +6616,12 @@ export interface Frame {
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
|
||||
* When passed a [string], matching is case-insensitive and searches for a substring.
|
||||
*/
|
||||
hasNotText?: string|RegExp;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
@ -10851,6 +10863,12 @@ export interface Locator {
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
|
||||
* When passed a [string], matching is case-insensitive and searches for a substring.
|
||||
*/
|
||||
hasNotText?: string|RegExp;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
@ -11507,6 +11525,12 @@ export interface Locator {
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
|
||||
* When passed a [string], matching is case-insensitive and searches for a substring.
|
||||
*/
|
||||
hasNotText?: string|RegExp;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
@ -17169,6 +17193,12 @@ export interface FrameLocator {
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain specified text somewhere inside, possibly in a child or a descendant element.
|
||||
* When passed a [string], matching is case-insensitive and searches for a substring.
|
||||
*/
|
||||
hasNotText?: string|RegExp;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
|
@ -59,6 +59,8 @@ it('should support playwright.locator.values', async ({ page }) => {
|
||||
expect(await page.evaluate(`playwright.locator('div', { hasText: /ELL/ }).elements.length`)).toBe(0);
|
||||
expect(await page.evaluate(`playwright.locator('div', { hasText: /ELL/i }).elements.length`)).toBe(1);
|
||||
expect(await page.evaluate(`playwright.locator('div', { hasText: /Hello/ }).elements.length`)).toBe(1);
|
||||
expect(await page.evaluate(`playwright.locator('div', { hasNotText: /Bar/ }).elements.length`)).toBe(0);
|
||||
expect(await page.evaluate(`playwright.locator('div', { hasNotText: /Hello/ }).elements.length`)).toBe(1);
|
||||
});
|
||||
|
||||
it('should support playwright.locator({ has })', async ({ page }) => {
|
||||
|
@ -291,6 +291,15 @@ it('reverse engineer hasText', async ({ page }) => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reverse engineer hasNotText', async ({ page }) => {
|
||||
expect.soft(generate(page.getByText('Hello').filter({ hasNotText: 'wo"rld\n' }))).toEqual({
|
||||
csharp: `GetByText("Hello").Filter(new() { HasNotText = "wo\\"rld\\n" })`,
|
||||
java: `getByText("Hello").filter(new Locator.FilterOptions().setHasNotText("wo\\"rld\\n"))`,
|
||||
javascript: `getByText('Hello').filter({ hasNotText: 'wo"rld\\n' })`,
|
||||
python: `get_by_text("Hello").filter(has_not_text="wo\\"rld\\n")`,
|
||||
});
|
||||
});
|
||||
|
||||
it('reverse engineer has', async ({ page }) => {
|
||||
expect.soft(generate(page.getByText('Hello').filter({ has: page.locator('div').getByText('bye') }))).toEqual({
|
||||
csharp: `GetByText("Hello").Filter(new() { Has = Locator("div").GetByText("bye") })`,
|
||||
@ -370,6 +379,7 @@ it.describe(() => {
|
||||
});
|
||||
|
||||
expect.soft(asLocator('javascript', 'div >> internal:has-text="foo"s', false)).toBe(`locator('div').locator('internal:has-text="foo"s')`);
|
||||
expect.soft(asLocator('javascript', 'div >> internal:has-not-text="foo"s', false)).toBe(`locator('div').locator('internal:has-not-text="foo"s')`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -160,6 +160,8 @@ it('should support locator.filter', async ({ page, trace }) => {
|
||||
await expect(page.locator(`div`).filter({ hasNot: page.locator('span', { hasText: 'world' }) })).toHaveCount(1);
|
||||
await expect(page.locator(`div`).filter({ hasNot: page.locator('section') })).toHaveCount(2);
|
||||
await expect(page.locator(`div`).filter({ hasNot: page.locator('span') })).toHaveCount(0);
|
||||
await expect(page.locator(`div`).filter({ hasNotText: 'hello' })).toHaveCount(1);
|
||||
await expect(page.locator(`div`).filter({ hasNotText: 'foo' })).toHaveCount(2);
|
||||
});
|
||||
|
||||
it('should support locator.or', async ({ page }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user