diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index f5da0a3995..0d2b7b677c 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -888,7 +888,7 @@ Value to set for the ``, `` or `[contenteditable]` element. ### option: Locator.fill.timeout = %%-input-timeout-js-%% * since: v1.14 -## method: Locator.filter +## method: Locator.filter#1 * since: v1.22 - returns: <[Locator]> @@ -946,9 +946,47 @@ await rowLocator .ScreenshotAsync(); ``` -### option: Locator.filter.-inline- = %%-locator-options-list-v1.14-%% +### option: Locator.filter#1.-inline- = %%-locator-options-list-v1.14-%% * since: v1.22 + +## method: Locator.filter#2 +* since: v1.33 +- 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').filter(page.getByTitle('Subscribe')); +``` + +```java +Locator button = page.getByRole(AriaRole.BUTTON).filter(page.getByTitle("Subscribe")); +``` + +```python async +button = page.get_by_role("button").filter(page.getByTitle("Subscribe")) +``` + +```python sync +button = page.get_by_role("button").filter(page.getByTitle("Subscribe")) +``` + +```csharp +var button = page.GetByRole(AriaRole.Button).Filter(page.GetByTitle("Subscribe")); +``` + +### param: Locator.filter#2.locator +* since: v1.33 +- `locator` <[Locator]> + +Additional locator to match. + + ## method: Locator.first * since: v1.14 - returns: <[Locator]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 2534012219..0a0dfee59f 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -1233,7 +1233,7 @@ Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-sele ## template-locator-locator -The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options, similar to [`method: Locator.filter`] method. +The method finds an element matching the specified selector in the locator's subtree. It also accepts filter options, similar to [`method: Locator.filter#1`] method. [Learn more about locators](../locators.md). @@ -1293,7 +1293,7 @@ use: { Allows locating elements that contain given text. -See also [`method: Locator.filter`] that allows to match by another criteria, like an accessible role, and then filter by the text content. +See also [`method: Locator.filter#1`] that allows to match by another criteria, like an accessible role, and then filter by the text content. **Usage** diff --git a/docs/src/locators.md b/docs/src/locators.md index 76d1ba4073..78d8b9b8ac 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -806,7 +806,7 @@ Consider the following DOM structure where we want to click on the buy button of ### Filter by text -Locators can be filtered by text with the [`method: Locator.filter`] method. It will search for a particular string somewhere inside the element, possibly in a descendant element, case-insensitively. You can also pass a regular expression. +Locators can be filtered by text with the [`method: Locator.filter#1`] method. It will search for a particular string somewhere inside the element, possibly in a descendant element, case-insensitively. You can also pass a regular expression. ```js await page @@ -883,7 +883,7 @@ await page .ClickAsync(); ``` -### Filter by another locator +### Filter by child/descendant Locators support an option to only select elements that have 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. @@ -985,6 +985,30 @@ 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.filter#2`] 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').filter(page.getByTitle('Subscribe')); +``` + +```java +Locator button = page.getByRole(AriaRole.BUTTON).filter(page.getByTitle("Subscribe")); +``` + +```python async +button = page.get_by_role("button").filter(page.getByTitle("Subscribe")) +``` + +```python sync +button = page.get_by_role("button").filter(page.getByTitle("Subscribe")) +``` + +```csharp +var button = page.GetByRole(AriaRole.Button).Filter(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. @@ -1198,7 +1222,7 @@ await page.GetByText("orange").ClickAsync(); ``` #### Filter by text -Use the [`method: Locator.filter`] to locate a specific item in a list. +Use the [`method: Locator.filter#1`] to locate a specific item in a list. For example, consider the following DOM structure: @@ -1303,7 +1327,7 @@ However, use this method with caution. Often times, the page might change, and t ### Chaining filters -When you have elements with various similarities, you can use the [`method: Locator.filter`] method to select the right one. You can also chain multiple filters to narrow down the selection. +When you have elements with various similarities, you can use the [`method: Locator.filter#1`] method to select the right one. You can also chain multiple filters to narrow down the selection. For example, consider the following DOM structure: diff --git a/docs/src/other-locators.md b/docs/src/other-locators.md index 97bedfe25b..ad86015237 100644 --- a/docs/src/other-locators.md +++ b/docs/src/other-locators.md @@ -967,7 +967,7 @@ If a selector needs to include `>>` in the body, it should be escaped inside a s ### Intermediate matches :::warning -We recommend [filtering by another locator](./locators.md#filter-by-another-locator) to locate elements that contain other elements. +We recommend [filtering by another locator](./locators.md#filter-by-childdescendant) to locate elements that contain other elements. ::: By default, chained selectors resolve to an element queried by the last selector. A selector can be prefixed with `*` to capture elements that are queried by an intermediate selector. diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index 2466ccce25..92f7b9eafc 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -452,7 +452,7 @@ Note that the new methods [`method: Page.routeFromHAR`] and [`method: BrowserCon Read more in [our documentation](./locators.md#locate-by-role). -- New [`method: Locator.filter`] API to filter an existing locator +- New [`method: Locator.filter#1`] API to filter an existing locator ```csharp var buttons = page.Locator("role=button"); diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index 5b5b3b1cf0..8eda83fbdd 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -385,7 +385,7 @@ Note that the new methods [`method: Page.routeFromHAR`] and [`method: BrowserCon Read more in [our documentation](./locators.md#locate-by-role). -- New [`method: Locator.filter`] API to filter an existing locator +- New [`method: Locator.filter#1`] API to filter an existing locator ```java Locator buttonsLocator = page.locator("role=button"); diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 7c55a255ee..8ee8ddba60 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -38,7 +38,7 @@ Note: **component tests only**, does not affect end-to-end tests. ### Browser Versions -* Chromium 112.0.5615.29 +* Chromium 112.0.5615.29 * Mozilla Firefox 111.0 * WebKit 16.4 @@ -786,7 +786,7 @@ WebServer is now considered "ready" if request to the specified port has any of Read more in [our documentation](./locators.md#locate-by-role). -- New [`method: Locator.filter`] API to filter an existing locator +- New [`method: Locator.filter#1`] API to filter an existing locator ```js const buttons = page.locator('role=button'); diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index 03452f61a9..aed0940f6a 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -440,7 +440,7 @@ Note that the new methods [`method: Page.routeFromHAR`] and [`method: BrowserCon Read more in [our documentation](./locators.md#locate-by-role). -- New [`method: Locator.filter`] API to filter an existing locator +- New [`method: Locator.filter#1`] API to filter an existing locator ```py buttons = page.locator("role=button") diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index ddfadc819a..3f2a5893f4 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -168,8 +168,15 @@ export class Locator implements api.Locator { return new FrameLocator(this._frame, this._selector + ' >> ' + selector); } - filter(options?: LocatorOptions): Locator { - return new Locator(this._frame, this._selector, options); + filter(options?: LocatorOptions): Locator; + filter(locator: Locator): Locator; + filter(optionsOrLocator?: LocatorOptions | Locator): Locator { + if (optionsOrLocator instanceof Locator) { + if (optionsOrLocator._frame !== this._frame) + throw new Error(`Locators must belong to the same frame.`); + return new Locator(this._frame, this._selector + ` >> internal:and=` + JSON.stringify(optionsOrLocator._selector)); + } + return new Locator(this._frame, this._selector, optionsOrLocator); } async elementHandle(options?: TimeoutOptions): Promise> { diff --git a/packages/playwright-core/src/server/injected/consoleApi.ts b/packages/playwright-core/src/server/injected/consoleApi.ts index 14d9f78ba7..57bb163c8c 100644 --- a/packages/playwright-core/src/server/injected/consoleApi.ts +++ b/packages/playwright-core/src/server/injected/consoleApi.ts @@ -53,7 +53,11 @@ class Locator { self.getByText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTextSelector(text, options)); self.getByTitle = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByTitleSelector(text, options)); self.getByRole = (role: string, options: ByRoleOptions = {}): Locator => self.locator(getByRoleSelector(role, options)); - self.filter = (options?: { hasText?: string | RegExp, has?: Locator }): Locator => new Locator(injectedScript, selector, options); + self.filter = (optionsOrLocator?: { hasText?: string | RegExp, has?: Locator } | Locator): Locator => { + if (optionsOrLocator instanceof Locator) + return new Locator(injectedScript, selectorBase + ` >> internal:and=` + JSON.stringify((optionsOrLocator as any)[selectorSymbol])); + return new Locator(injectedScript, selector, optionsOrLocator); + }; self.first = (): Locator => self.locator('nth=0'); self.last = (): Locator => self.locator('nth=-1'); self.nth = (index: number): Locator => self.locator(`nth=${index}`); diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 449f52ff14..e4dbdcd875 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -114,6 +114,7 @@ export class InjectedScript { this._engines.set('internal:control', this._createControlEngine()); this._engines.set('internal:has', this._createHasEngine()); this._engines.set('internal:or', { queryAll: () => [] }); + this._engines.set('internal:and', { 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()); @@ -170,14 +171,6 @@ export class InjectedScript { return new Set(list.slice(nth, nth + 1)); } - private _queryOr(elements: Set, part: ParsedSelectorPart): Set { - const list = [...elements]; - let nth = +part.body; - if (nth === -1) - nth = list.length - 1; - return new Set(list.slice(nth, nth + 1)); - } - private _queryLayoutSelector(elements: Set, part: ParsedSelectorPart, originalRoot: Node): Set { const name = part.name as LayoutSelectorName; const body = part.body as NestedSelectorBody; @@ -222,6 +215,9 @@ 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 (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 49fecbb9f3..ef27059bf0 100644 --- a/packages/playwright-core/src/server/selectors.ts +++ b/packages/playwright-core/src/server/selectors.ts @@ -35,7 +35,7 @@ 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-text', 'internal:or', + 'nth', 'visible', 'internal:control', 'internal:has', 'internal:has-text', 'internal:or', 'internal:and', '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 fa3642a3e9..600c6a66e4 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' | 'frame' | 'or'; +export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has' | 'frame' | 'or' | 'and'; export type LocatorBase = 'page' | 'locator' | 'frame-locator'; type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp }; @@ -91,6 +91,11 @@ 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:label') { const { exact, text } = detectExact(part.body as string); tokens.push(factory.generateLocator(base, 'label', text, { exact })); @@ -202,6 +207,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory { return `filter({ has: ${body} })`; case 'or': return `or(${body})`; + case 'and': + return `filter(${body})`; case 'test-id': return `getByTestId(${this.quote(body as string)})`; case 'text': @@ -272,6 +279,8 @@ export class PythonLocatorFactory implements LocatorFactory { return `filter(has=${body})`; case 'or': return `or_(${body})`; + case 'and': + return `filter(${body})`; case 'test-id': return `get_by_test_id(${this.quote(body as string)})`; case 'text': @@ -351,6 +360,8 @@ export class JavaLocatorFactory implements LocatorFactory { return `filter(new ${clazz}.FilterOptions().setHas(${body}))`; case 'or': return `or(${body})`; + case 'and': + return `filter(${body})`; case 'test-id': return `getByTestId(${this.quote(body as string)})`; case 'text': @@ -424,6 +435,8 @@ export class CSharpLocatorFactory implements LocatorFactory { return `Filter(new() { Has = ${body} })`; case 'or': return `Or(${body})`; + case 'and': + return `Filter(${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 486d36241c..f06d2d759a 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorParser.ts @@ -102,7 +102,7 @@ function shiftParams(template: string, sub: number) { function transform(template: string, params: TemplateParams, testIdAttributeName: string): string { // Recursively handle filter(has=). - // TODO: handle or(locator) as well. + // TODO: handle or(locator) and filter(locator). while (true) { const hasMatch = template.match(/filter\(,?has=/); if (!hasMatch) diff --git a/packages/playwright-core/src/utils/isomorphic/selectorParser.ts b/packages/playwright-core/src/utils/isomorphic/selectorParser.ts index 41ff749a30..381aa5326b 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:or', 'left-of', 'right-of', 'above', 'below', 'near']); +const kNestedSelectorNames = new Set(['internal:has', 'internal:or', 'internal:and', '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 dfe7f0f4e7..676fd439e2 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2686,7 +2686,7 @@ export interface Page { /** * Allows locating elements that contain given text. * - * See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to + * See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) that allows to * match by another criteria, like an accessible role, and then filter by the text content. * * **Usage** @@ -6107,7 +6107,7 @@ export interface Frame { /** * Allows locating elements that contain given text. * - * See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to + * See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) that allows to * match by another criteria, like an accessible role, and then filter by the text content. * * **Usage** @@ -10785,6 +10785,21 @@ export interface Locator { hasText?: string|RegExp; }): 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').filter(page.getByTitle('Subscribe')); + * ``` + * + * @param locator Additional locator to match. + */ + filter(locator: Locator): Locator; + /** * Returns locator to the first matching element. */ @@ -11065,7 +11080,7 @@ export interface Locator { /** * Allows locating elements that contain given text. * - * See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to + * See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) that allows to * match by another criteria, like an accessible role, and then filter by the text content. * * **Usage** @@ -11409,7 +11424,7 @@ export interface Locator { /** * The method finds an element matching the specified selector in the locator's subtree. It also accepts filter - * options, similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) + * options, similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) * method. * * [Learn more about locators](https://playwright.dev/docs/locators). @@ -16952,7 +16967,7 @@ export interface FrameLocator { /** * Allows locating elements that contain given text. * - * See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to + * See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) that allows to * match by another criteria, like an accessible role, and then filter by the text content. * * **Usage** @@ -17036,7 +17051,7 @@ export interface FrameLocator { /** * The method finds an element matching the specified selector in the locator's subtree. It also accepts filter - * options, similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) + * options, similar to [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter-1) * method. * * [Learn more about locators](https://playwright.dev/docs/locators). diff --git a/tests/library/inspector/console-api.spec.ts b/tests/library/inspector/console-api.spec.ts index 6e1dc7b430..b99a143276 100644 --- a/tests/library/inspector/console-api.spec.ts +++ b/tests/library/inspector/console-api.spec.ts @@ -67,11 +67,16 @@ it('should support playwright.locator({ has })', async ({ page }) => { expect(await page.evaluate(`playwright.locator('div', { has: playwright.locator('text=Hello') }).element.innerHTML`)).toContain('span'); }); -it('should support playwright.or()', async ({ page }) => { +it('should support locator.or()', async ({ page }) => { await page.setContent('HiHello'); expect(await page.evaluate(`playwright.locator('div').or(playwright.locator('span')).elements.map(e => e.innerHTML)`)).toEqual(['Hi', 'Hello']); }); +it('should support locator.filter(locator)', async ({ page }) => { + await page.setContent('Hi'); + expect(await page.evaluate(`playwright.locator('div').filter(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 72ea285933..8c35b5b7f0 100644 --- a/tests/library/locator-generator.spec.ts +++ b/tests/library/locator-generator.spec.ts @@ -359,6 +359,13 @@ 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').filter(locator('span').locator('article'))`); + expect.soft(asLocator('python', 'div >> internal:and="span >> article"', false)).toBe(`locator("div").filter(locator("span").locator("article"))`); + expect.soft(asLocator('java', 'div >> internal:and="span >> article"', false)).toBe(`locator("div").filter(locator("span").locator("article"))`); + expect.soft(asLocator('csharp', 'div >> internal:and="span >> article"', false)).toBe(`Locator("div").Filter(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 f811b8118e..55f7ae0e48 100644 --- a/tests/page/locator-query.spec.ts +++ b/tests/page/locator-query.spec.ts @@ -171,6 +171,19 @@ it('should support locator.or', async ({ page }) => { await expect(page.locator('span').or(page.locator('article'))).toHaveText('world'); }); +it('should support locator.filter(locator)', async ({ page }) => { + await page.setContent(` + helloworld + hello2world2 + `); + await expect(page.locator('div').filter(page.locator('div'))).toHaveCount(2); + await expect(page.locator('div').filter(page.getByTestId('foo'))).toHaveText(['hello']); + await expect(page.locator('div').filter(page.getByTestId('bar'))).toHaveText(['world']); + await expect(page.getByTestId('foo').filter(page.locator('div'))).toHaveText(['hello']); + await expect(page.getByTestId('bar').filter(page.locator('span'))).toHaveText(['world2']); + await expect(page.locator('span').filter(page.getByTestId(/bar|foo/))).toHaveCount(2); +}); + 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 6b40c1d059..d9069357db 100644 --- a/tests/page/selectors-misc.spec.ts +++ b/tests/page/selectors-misc.spec.ts @@ -414,6 +414,19 @@ 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(` + helloworld + 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('chaining should work with large DOM @smoke', async ({ page, server }) => { await page.evaluate(() => { let last = document.body; diff --git a/utils/doclint/missingDocs.js b/utils/doclint/missingDocs.js index 1baad4360e..8042bc4222 100644 --- a/utils/doclint/missingDocs.js +++ b/utils/doclint/missingDocs.js @@ -35,14 +35,14 @@ module.exports = function lint(documentation, jsSources, apiFileName) { continue; } for (const [methodName, params] of methods) { - const member = docClass.membersArray.find(m => m.alias === methodName && m.kind !== 'event'); - if (!member) { + const members = docClass.membersArray.filter(m => m.alias === methodName && m.kind !== 'event'); + if (!members.length) { errors.push(`Missing documentation for "${className}.${methodName}"`); continue; } - const memberParams = paramsForMember(member); for (const paramName of params) { - if (!memberParams.has(paramName)) + const found = members.some(member => paramsForMember(member).has(paramName)); + if (!found) errors.push(`Missing documentation for "${className}.${methodName}.${paramName}"`); } }