diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 6e2c73b3a6..4e1937f57f 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -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 @@ -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 diff --git a/docs/src/locators.md b/docs/src/locators.md index 7a47889ed7..dea4ca1a73 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -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. diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 9d76d07b9a..7ffbab7d18 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -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'); } @@ -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.`); diff --git a/packages/playwright-core/src/server/injected/consoleApi.ts b/packages/playwright-core/src/server/injected/consoleApi.ts index b84f89e59c..3aff5a1ffa 100644 --- a/packages/playwright-core/src/server/injected/consoleApi.ts +++ b/packages/playwright-core/src/server/injected/consoleApi.ts @@ -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])); } } @@ -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) { diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index a28d421b77..963b66c651 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -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()); @@ -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 { diff --git a/packages/playwright-core/src/server/selectors.ts b/packages/playwright-core/src/server/selectors.ts index e2add1caa9..13ad30a08b 100644 --- a/packages/playwright-core/src/server/selectors.ts +++ b/packages/playwright-core/src/server/selectors.ts @@ -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([ diff --git a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts index b90640bf57..c5fea64c1c 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts @@ -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 }; @@ -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 })); @@ -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': @@ -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': @@ -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': @@ -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': diff --git a/packages/playwright-core/src/utils/isomorphic/locatorParser.ts b/packages/playwright-core/src/utils/isomorphic/locatorParser.ts index c787a5657c..aabe0938dc 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorParser.ts @@ -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') @@ -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) diff --git a/packages/playwright-core/src/utils/isomorphic/selectorParser.ts b/packages/playwright-core/src/utils/isomorphic/selectorParser.ts index 75130a098d..34f16f5c2c 100644 --- a/packages/playwright-core/src/utils/isomorphic/selectorParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/selectorParser.ts @@ -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 = { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d81bbb18ce..d15abf6d95 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -10240,21 +10240,6 @@ export interface Locator { */ allTextContents(): Promise>; - /** - * 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 @@ -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. * diff --git a/tests/library/inspector/console-api.spec.ts b/tests/library/inspector/console-api.spec.ts index 8311ca56be..0d54019c7d 100644 --- a/tests/library/inspector/console-api.spec.ts +++ b/tests/library/inspector/console-api.spec.ts @@ -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('
Hi
Hello
'); - 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('
Hi
'); - 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('HelloWorld'); expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello'); diff --git a/tests/library/locator-generator.spec.ts b/tests/library/locator-generator.spec.ts index 668f823ffe..6a403b0791 100644 --- a/tests/library/locator-generator.spec.ts +++ b/tests/library/locator-generator.spec.ts @@ -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'; diff --git a/tests/page/locator-query.spec.ts b/tests/page/locator-query.spec.ts index 34bc394b9f..93a1967f8d 100644 --- a/tests/page/locator-query.spec.ts +++ b/tests/page/locator-query.spec.ts @@ -176,30 +176,6 @@ it('should support locator.or', async ({ page }) => { await expect(page.locator('span').or(page.locator('article'))).toHaveText('world'); }); -it('should support locator.and', async ({ page }) => { - await page.setContent(` -
hello
world
- hello2world2 - `); - await expect(page.locator('div').and(page.locator('div'))).toHaveCount(2); - await expect(page.locator('div').and(page.getByTestId('foo'))).toHaveText(['hello']); - await expect(page.locator('div').and(page.getByTestId('bar'))).toHaveText(['world']); - await expect(page.getByTestId('foo').and(page.locator('div'))).toHaveText(['hello']); - await expect(page.getByTestId('bar').and(page.locator('span'))).toHaveText(['world2']); - await expect(page.locator('span').and(page.getByTestId(/bar|foo/))).toHaveCount(2); -}); - -it('should support locator.not', async ({ page }) => { - await page.setContent(`
hello
world
`); - await expect(page.locator('div').not(page.locator('span'))).toHaveCount(2); - await expect(page.locator('div').not(page.locator('span'))).toHaveText(['hello', 'world']); - await expect(page.locator('div').not(page.locator('.foo'))).toHaveText(['world']); - await expect(page.locator('div').not(page.locator('.bar'))).toHaveText(['hello']); - await expect(page.locator('.foo').not(page.locator('.bar'))).toHaveText(['hello']); - await expect(page.locator('.foo').not(page.locator('div'))).toHaveText([]); - await expect(page.locator('div').not(page.locator('div'))).toHaveText([]); -}); - it('should enforce same frame for has/leftOf/rightOf/above/below/near', async ({ page, server }) => { await page.goto(server.PREFIX + '/frames/two-frames.html'); const child = page.frames()[1]; diff --git a/tests/page/selectors-misc.spec.ts b/tests/page/selectors-misc.spec.ts index dcd820658a..461c30a62d 100644 --- a/tests/page/selectors-misc.spec.ts +++ b/tests/page/selectors-misc.spec.ts @@ -423,32 +423,6 @@ it('should work with internal:or=', async ({ page, server }) => { expect(await page.locator(`span >> internal:or="article"`).textContent()).toBe('world'); }); -it('should work with internal:and=', async ({ page, server }) => { - await page.setContent(` -
hello
world
- hello2world2 - `); - expect(await page.$$eval(`div >> internal:and="span"`, els => els.map(e => e.textContent))).toEqual([]); - expect(await page.$$eval(`div >> internal:and=".foo"`, els => els.map(e => e.textContent))).toEqual(['hello']); - expect(await page.$$eval(`div >> internal:and=".bar"`, els => els.map(e => e.textContent))).toEqual(['world']); - expect(await page.$$eval(`span >> internal:and="span"`, els => els.map(e => e.textContent))).toEqual(['hello2', 'world2']); - expect(await page.$$eval(`.foo >> internal:and="div"`, els => els.map(e => e.textContent))).toEqual(['hello']); - expect(await page.$$eval(`.bar >> internal:and="span"`, els => els.map(e => e.textContent))).toEqual(['world2']); -}); - -it('should work with internal:not=', async ({ page, server }) => { - await page.setContent(` -
hello
-
world
- `); - expect(await page.$$eval(`div >> internal:not="span"`, els => els.map(e => e.textContent))).toEqual(['hello', 'world']); - expect(await page.$$eval(`div >> internal:not=".foo"`, els => els.map(e => e.textContent))).toEqual(['world']); - expect(await page.$$eval(`div >> internal:not=".bar"`, els => els.map(e => e.textContent))).toEqual(['hello']); - expect(await page.$$eval(`div >> internal:not="div"`, els => els.map(e => e.textContent))).toEqual([]); - expect(await page.$$eval(`span >> internal:not="div"`, els => els.map(e => e.textContent))).toEqual([]); - expect(await page.$$eval(`.foo >> internal:not=".bar"`, els => els.map(e => e.textContent))).toEqual(['hello']); -}); - it('chaining should work with large DOM @smoke', async ({ page, server }) => { await page.evaluate(() => { let last = document.body;