Skip to content

Commit

Permalink
feat(locator): remove locator.and and locator.not (#22223)
Browse files Browse the repository at this point in the history
Not shipping for now, after API review.
  • Loading branch information
dgozman committed Apr 5, 2023
1 parent b519512 commit 08cef43
Show file tree
Hide file tree
Showing 14 changed files with 4 additions and 283 deletions.
76 changes: 0 additions & 76 deletions docs/src/api/class-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,44 +98,6 @@ String[] texts = page.getByRole(AriaRole.LINK).allTextContents();
var texts = await page.GetByRole(AriaRole.Link).AllTextContentsAsync();
```

## method: Locator.and
* since: v1.33
* langs:
- alias-python: and_
- returns: <[Locator]>

Creates a locator that matches both this locator and the argument locator.

**Usage**

The following example finds a button with a specific title.

```js
const button = page.getByRole('button').and(page.getByTitle('Subscribe'));
```

```java
Locator button = page.getByRole(AriaRole.BUTTON).and(page.getByTitle("Subscribe"));
```

```python async
button = page.get_by_role("button").and_(page.getByTitle("Subscribe"))
```

```python sync
button = page.get_by_role("button").and_(page.getByTitle("Subscribe"))
```

```csharp
var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe"));
```

### param: Locator.and.locator
* since: v1.33
- `locator` <[Locator]>

Additional locator to match.


## async method: Locator.blur
* since: v1.28
Expand Down Expand Up @@ -1514,44 +1476,6 @@ var banana = await page.GetByRole(AriaRole.Listitem).Last(1);
### option: Locator.locator.hasNotText = %%-locator-option-has-not-text-%%
* since: v1.33

## method: Locator.not
* since: v1.33
* langs:
- alias-python: not_
- returns: <[Locator]>

Creates a locator that **matches this** locator, but **not the argument** locator.

**Usage**

The following example finds a button that does not have title `"Subscribe"`.

```js
const button = page.getByRole('button').not(page.getByTitle('Subscribe'));
```

```java
Locator button = page.getByRole(AriaRole.BUTTON).not(page.getByTitle("Subscribe"));
```

```python async
button = page.get_by_role("button").not_(page.getByTitle("Subscribe"))
```

```python sync
button = page.get_by_role("button").not_(page.getByTitle("Subscribe"))
```

```csharp
var button = page.GetByRole(AriaRole.Button).Not(page.GetByTitle("Subscribe"));
```

### param: Locator.not.locator
* since: v1.33
- `locator` <[Locator]>

Locator that must not match.


## method: Locator.nth
* since: v1.14
Expand Down
48 changes: 0 additions & 48 deletions docs/src/locators.md
Original file line number Diff line number Diff line change
Expand Up @@ -1056,54 +1056,6 @@ await Expect(page

Note that the inner locator is matched starting from the outer one, not from the document root.

### Filter by matching an additional locator

Method [`method: Locator.and`] narrows down an existing locator by matching an additional locator. For example, you can combine [`method: Page.getByRole`] and [`method: Page.getByTitle`] to match by both role and title.

```js
const button = page.getByRole('button').and(page.getByTitle('Subscribe'));
```

```java
Locator button = page.getByRole(AriaRole.BUTTON).and(page.getByTitle("Subscribe"));
```

```python async
button = page.get_by_role("button").and_(page.getByTitle("Subscribe"))
```

```python sync
button = page.get_by_role("button").and_(page.getByTitle("Subscribe"))
```

```csharp
var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe"));
```

### Filter by **not** matching an additional locator

Method [`method: Locator.not`] narrows down an existing locator by ensuring that target element **does not match** an additional locator. For example, you can combine [`method: Page.getByRole`] and [`method: Page.getByTitle`] to match by role and ensure that title does not match.

```js
const button = page.getByRole('button').not(page.getByTitle('Subscribe'));
```

```java
Locator button = page.getByRole(AriaRole.BUTTON).not(page.getByTitle("Subscribe"));
```

```python async
button = page.get_by_role("button").not_(page.getByTitle("Subscribe"))
```

```python sync
button = page.get_by_role("button").not_(page.getByTitle("Subscribe"))
```

```csharp
var button = page.GetByRole(AriaRole.Button).Not(page.GetByTitle("Subscribe"));
```

## Chaining Locators

You can chain methods that create a locator, like [`method: Page.getByText`] or [`method: Locator.getByRole`], to narrow down the search to a particular part of the page.
Expand Down
12 changes: 0 additions & 12 deletions packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,6 @@ export class Locator implements api.Locator {
return this._frame.$$(this._selector);
}

and(locator: Locator): Locator {
if (locator._frame !== this._frame)
throw new Error(`Locators must belong to the same frame.`);
return new Locator(this._frame, this._selector + ` >> internal:and=` + JSON.stringify(locator._selector));
}

first(): Locator {
return new Locator(this._frame, this._selector + ' >> nth=0');
}
Expand All @@ -210,12 +204,6 @@ export class Locator implements api.Locator {
return new Locator(this._frame, this._selector + ` >> nth=${index}`);
}

not(locator: Locator): Locator {
if (locator._frame !== this._frame)
throw new Error(`Locators must belong to the same frame.`);
return new Locator(this._frame, this._selector + ` >> internal:not=` + JSON.stringify(locator._selector));
}

or(locator: Locator): Locator {
if (locator._frame !== this._frame)
throw new Error(`Locators must belong to the same frame.`);
Expand Down
4 changes: 0 additions & 4 deletions packages/playwright-core/src/server/injected/consoleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ class Locator {
self.first = (): Locator => self.locator('nth=0');
self.last = (): Locator => self.locator('nth=-1');
self.nth = (index: number): Locator => self.locator(`nth=${index}`);
self.and = (locator: Locator): Locator => new Locator(injectedScript, selectorBase + ` >> internal:and=` + JSON.stringify((locator as any)[selectorSymbol]));
self.or = (locator: Locator): Locator => new Locator(injectedScript, selectorBase + ` >> internal:or=` + JSON.stringify((locator as any)[selectorSymbol]));
self.not = (locator: Locator): Locator => new Locator(injectedScript, selectorBase + ` >> internal:not=` + JSON.stringify((locator as any)[selectorSymbol]));
}
}

Expand Down Expand Up @@ -95,9 +93,7 @@ class ConsoleAPI {
delete this._injectedScript.window.playwright.first;
delete this._injectedScript.window.playwright.last;
delete this._injectedScript.window.playwright.nth;
delete this._injectedScript.window.playwright.and;
delete this._injectedScript.window.playwright.or;
delete this._injectedScript.window.playwright.not;
}

private _querySelector(selector: string, strict: boolean): (Element | undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,6 @@ export class InjectedScript {
this._engines.set('internal:has', this._createHasEngine());
this._engines.set('internal:has-not', this._createHasNotEngine());
this._engines.set('internal:or', { queryAll: () => [] });
this._engines.set('internal:and', { queryAll: () => [] });
this._engines.set('internal:not', { queryAll: () => [] });
this._engines.set('internal:label', this._createInternalLabelEngine());
this._engines.set('internal:text', this._createTextEngine(true, true));
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
Expand Down Expand Up @@ -217,12 +215,6 @@ export class InjectedScript {
} else if (part.name === 'internal:or') {
const orElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root);
roots = new Set(sortInDOMOrder(new Set([...roots, ...orElements])));
} else if (part.name === 'internal:and') {
const andElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root);
roots = new Set(andElements.filter(e => roots.has(e)));
} else if (part.name === 'internal:not') {
const notElements = new Set(this.querySelectorAll((part.body as NestedSelectorBody).parsed, root));
roots = new Set([...roots].filter(e => !notElements.has(e)));
} else if (kLayoutSelectorNames.includes(part.name as LayoutSelectorName)) {
roots = this._queryLayoutSelector(roots, part, root);
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class Selectors {
'nth', 'visible', 'internal:control',
'internal:has', 'internal:has-not',
'internal:has-text', 'internal:has-not-text',
'internal:or', 'internal:and', 'internal:not',
'internal:or',
'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role', 'internal:testid',
]);
this._builtinEnginesInMainWorld = new Set([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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-not-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';
export type LocatorBase = 'page' | 'locator' | 'frame-locator';

type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp };
Expand Down Expand Up @@ -104,16 +104,6 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
tokens.push(factory.generateLocator(base, 'or', inner));
continue;
}
if (part.name === 'internal:and') {
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
tokens.push(factory.generateLocator(base, 'and', inner));
continue;
}
if (part.name === 'internal:not') {
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
tokens.push(factory.generateLocator(base, 'not', inner));
continue;
}
if (part.name === 'internal:label') {
const { exact, text } = detectExact(part.body as string);
tokens.push(factory.generateLocator(base, 'label', text, { exact }));
Expand Down Expand Up @@ -229,10 +219,6 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
return `filter({ hasNot: ${body} })`;
case 'or':
return `or(${body})`;
case 'and':
return `and(${body})`;
case 'not':
return `not(${body})`;
case 'test-id':
return `getByTestId(${this.quote(body as string)})`;
case 'text':
Expand Down Expand Up @@ -307,10 +293,6 @@ export class PythonLocatorFactory implements LocatorFactory {
return `filter(has_not=${body})`;
case 'or':
return `or_(${body})`;
case 'and':
return `and_(${body})`;
case 'not':
return `not_(${body})`;
case 'test-id':
return `get_by_test_id(${this.quote(body as string)})`;
case 'text':
Expand Down Expand Up @@ -394,10 +376,6 @@ export class JavaLocatorFactory implements LocatorFactory {
return `filter(new ${clazz}.FilterOptions().setHasNot(${body}))`;
case 'or':
return `or(${body})`;
case 'and':
return `and(${body})`;
case 'not':
return `not(${body})`;
case 'test-id':
return `getByTestId(${this.quote(body as string)})`;
case 'text':
Expand Down Expand Up @@ -475,10 +453,6 @@ export class CSharpLocatorFactory implements LocatorFactory {
return `Filter(new() { HasNot = ${body} })`;
case 'or':
return `Or(${body})`;
case 'and':
return `And(${body})`;
case 'not':
return `Not(${body})`;
case 'test-id':
return `GetByTestId(${this.quote(body as string)})`;
case 'text':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ function parseLocator(locator: string, testIdAttributeName: string): string {
.replace(/new[\w]+\.[\w]+options\(\)/g, '')
.replace(/\.set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase())
.replace(/\.or_\(/g, 'or(') // Python has "or_" instead of "or".
.replace(/\.not_\(/g, 'not(') // Python has "not_" instead of "not".
.replace(/:/g, '=')
.replace(/,re\.ignorecase/g, 'i')
.replace(/,pattern.case_insensitive/g, 'i')
Expand All @@ -105,7 +104,7 @@ function shiftParams(template: string, sub: number) {

function transform(template: string, params: TemplateParams, testIdAttributeName: string): string {
// Recursively handle filter(has=, hasnot=).
// TODO: handle or(locator), not(locator), and(locator).
// TODO: handle or(locator).
while (true) {
const hasMatch = template.match(/filter\(,?(has|hasnot)=/);
if (!hasMatch)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { InvalidSelectorError, parseCSS } from './cssParser';
export { InvalidSelectorError, isInvalidSelectorError } from './cssParser';

export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number };
const kNestedSelectorNames = new Set(['internal:has', 'internal:has-not', 'internal:or', 'internal:and', 'internal:not', 'left-of', 'right-of', 'above', 'below', 'near']);
const kNestedSelectorNames = new Set(['internal:has', 'internal:has-not', 'internal:or', 'left-of', 'right-of', 'above', 'below', 'near']);
const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);

export type ParsedSelectorPart = {
Expand Down
30 changes: 0 additions & 30 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10240,21 +10240,6 @@ export interface Locator {
*/
allTextContents(): Promise<Array<string>>;

/**
* Creates a locator that matches both this locator and the argument locator.
*
* **Usage**
*
* The following example finds a button with a specific title.
*
* ```js
* const button = page.getByRole('button').and(page.getByTitle('Subscribe'));
* ```
*
* @param locator Additional locator to match.
*/
and(locator: Locator): Locator;

/**
* Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) on the element.
* @param options
Expand Down Expand Up @@ -11539,21 +11524,6 @@ export interface Locator {
hasText?: string|RegExp;
}): Locator;

/**
* Creates a locator that **matches this** locator, but **not the argument** locator.
*
* **Usage**
*
* The following example finds a button that does not have title `"Subscribe"`.
*
* ```js
* const button = page.getByRole('button').not(page.getByTitle('Subscribe'));
* ```
*
* @param locator Locator that must not match.
*/
not(locator: Locator): Locator;

/**
* Returns locator to the n-th matching element. It's zero based, `nth(0)` selects the first element.
*
Expand Down
10 changes: 0 additions & 10 deletions tests/library/inspector/console-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,6 @@ it('should support locator.or()', async ({ page }) => {
expect(await page.evaluate(`playwright.locator('div').or(playwright.locator('span')).elements.map(e => e.innerHTML)`)).toEqual(['Hi', 'Hello']);
});

it('should support locator.not()', async ({ page }) => {
await page.setContent('<div class=foo>Hi</div><div class=bar>Hello</div>');
expect(await page.evaluate(`playwright.locator('div').not(playwright.locator('.foo')).elements.map(e => e.innerHTML)`)).toEqual(['Hello']);
});

it('should support locator.and()', async ({ page }) => {
await page.setContent('<div data-testid=Hey>Hi</div>');
expect(await page.evaluate(`playwright.locator('div').and(playwright.getByTestId('Hey')).elements.map(e => e.innerHTML)`)).toEqual(['Hi']);
});

it('should support playwright.getBy*', async ({ page }) => {
await page.setContent('<span>Hello</span><span title="world">World</span>');
expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello');
Expand Down
14 changes: 0 additions & 14 deletions tests/library/locator-generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,20 +390,6 @@ it('asLocator internal:or', async () => {
expect.soft(asLocator('csharp', 'div >> internal:or="span >> article"', false)).toBe(`Locator("div").Or(Locator("span").Locator("article"))`);
});

it('asLocator internal:and', async () => {
expect.soft(asLocator('javascript', 'div >> internal:and="span >> article"', false)).toBe(`locator('div').and(locator('span').locator('article'))`);
expect.soft(asLocator('python', 'div >> internal:and="span >> article"', false)).toBe(`locator("div").and_(locator("span").locator("article"))`);
expect.soft(asLocator('java', 'div >> internal:and="span >> article"', false)).toBe(`locator("div").and(locator("span").locator("article"))`);
expect.soft(asLocator('csharp', 'div >> internal:and="span >> article"', false)).toBe(`Locator("div").And(Locator("span").Locator("article"))`);
});

it('asLocator internal:not', async () => {
expect.soft(asLocator('javascript', 'div >> internal:not="span >> article"', false)).toBe(`locator('div').not(locator('span').locator('article'))`);
expect.soft(asLocator('python', 'div >> internal:not="span >> article"', false)).toBe(`locator("div").not_(locator("span").locator("article"))`);
expect.soft(asLocator('java', 'div >> internal:not="span >> article"', false)).toBe(`locator("div").not(locator("span").locator("article"))`);
expect.soft(asLocator('csharp', 'div >> internal:not="span >> article"', false)).toBe(`Locator("div").Not(Locator("span").Locator("article"))`);
});

it('parse locators strictly', () => {
const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span';

Expand Down

0 comments on commit 08cef43

Please sign in to comment.