feat: Locator.or(locator) (#21884)
This commit is contained in:
parent
6947f47f05
commit
d10fac4f6a
|
|
@ -1495,6 +1495,51 @@ var banana = await page.GetByRole(AriaRole.Listitem).Nth(2);
|
||||||
* since: v1.14
|
* since: v1.14
|
||||||
- `index` <[int]>
|
- `index` <[int]>
|
||||||
|
|
||||||
|
|
||||||
|
## method: Locator.or
|
||||||
|
* since: v1.33
|
||||||
|
* langs:
|
||||||
|
- alias-python: or_
|
||||||
|
- returns: <[Locator]>
|
||||||
|
|
||||||
|
Creates a locator that matches either of the two locators.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
If your page shows a username input that is labelled either `Username` or `Login`, depending on some external factors you do not control, you can match both.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const input = page.getByLabel('Username').or(page.getByLabel('Login'));
|
||||||
|
await input.fill('John');
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
Locator input = page.getByLabel("Username").or(page.getByLabel("Login"));
|
||||||
|
input.fill("John");
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
input = page.get_by_label("Username").or_(page.get_by_label("Login"))
|
||||||
|
await input.fill("John")
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
input = page.get_by_label("Username").or_(page.get_by_label("Login"))
|
||||||
|
input.fill("John")
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var input = page.GetByLabel("Username").Or(page.GetByLabel("Login"));
|
||||||
|
await input.FillAsync("John");
|
||||||
|
```
|
||||||
|
|
||||||
|
### param: Locator.or.locator
|
||||||
|
* since: v1.33
|
||||||
|
- `locator` <[Locator]>
|
||||||
|
|
||||||
|
Alternative locator to match.
|
||||||
|
|
||||||
|
|
||||||
## method: Locator.page
|
## method: Locator.page
|
||||||
* since: v1.19
|
* since: v1.19
|
||||||
- returns: <[Page]>
|
- returns: <[Page]>
|
||||||
|
|
|
||||||
|
|
@ -457,7 +457,40 @@ var parentLocator = page.GetByRole(AriaRole.Button).Locator("..");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Locating only visible elements
|
|
||||||
|
## Combining two alternative locators
|
||||||
|
|
||||||
|
If you'd like to target one of the two or more elements, and you don't know which one it will be, use [`method: Locator.or`] to create a locator that matches any of the alternatives.
|
||||||
|
|
||||||
|
For example, to fill the username input that is labelled either `Username` or `Login`, depending on some external factors:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const input = page.getByLabel('Username').or(page.getByLabel('Login'));
|
||||||
|
await input.fill('John');
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
Locator input = page.getByLabel("Username").or(page.getByLabel("Login"));
|
||||||
|
input.fill("John");
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
input = page.get_by_label("Username").or_(page.get_by_label("Login"))
|
||||||
|
await input.fill("John")
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
input = page.get_by_label("Username").or_(page.get_by_label("Login"))
|
||||||
|
input.fill("John")
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var input = page.GetByLabel("Username").Or(page.GetByLabel("Login"));
|
||||||
|
await input.FillAsync("John");
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Locating only visible elements
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
It's usually better to find a [more reliable way](./locators.md#quick-guide) to uniquely identify the element instead of checking the visibility.
|
It's usually better to find a [more reliable way](./locators.md#quick-guide) to uniquely identify the element instead of checking the visibility.
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,12 @@ export class Locator implements api.Locator {
|
||||||
return new Locator(this._frame, this._selector + ` >> nth=${index}`);
|
return new Locator(this._frame, this._selector + ` >> nth=${index}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
or(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:or=` + JSON.stringify(locator._selector));
|
||||||
|
}
|
||||||
|
|
||||||
async focus(options?: TimeoutOptions): Promise<void> {
|
async focus(options?: TimeoutOptions): Promise<void> {
|
||||||
return this._frame.focus(this._selector, { strict: true, ...options });
|
return this._frame.focus(this._selector, { strict: true, ...options });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ class Locator {
|
||||||
self.first = (): Locator => self.locator('nth=0');
|
self.first = (): Locator => self.locator('nth=0');
|
||||||
self.last = (): Locator => self.locator('nth=-1');
|
self.last = (): Locator => self.locator('nth=-1');
|
||||||
self.nth = (index: number): Locator => self.locator(`nth=${index}`);
|
self.nth = (index: number): Locator => self.locator(`nth=${index}`);
|
||||||
|
self.or = (locator: Locator): Locator => new Locator(injectedScript, selectorBase + ` >> internal:or=` + JSON.stringify((locator as any)[selectorSymbol]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,6 +89,7 @@ class ConsoleAPI {
|
||||||
delete this._injectedScript.window.playwright.first;
|
delete this._injectedScript.window.playwright.first;
|
||||||
delete this._injectedScript.window.playwright.last;
|
delete this._injectedScript.window.playwright.last;
|
||||||
delete this._injectedScript.window.playwright.nth;
|
delete this._injectedScript.window.playwright.nth;
|
||||||
|
delete this._injectedScript.window.playwright.or;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _querySelector(selector: string, strict: boolean): (Element | undefined) {
|
private _querySelector(selector: string, strict: boolean): (Element | undefined) {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { parseAttributeSelector } from '../../utils/isomorphic/selectorParser';
|
||||||
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../../utils/isomorphic/selectorParser';
|
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../../utils/isomorphic/selectorParser';
|
||||||
import { allEngineNames, parseSelector, stringifySelector } from '../../utils/isomorphic/selectorParser';
|
import { allEngineNames, parseSelector, stringifySelector } from '../../utils/isomorphic/selectorParser';
|
||||||
import { type TextMatcher, elementMatchesText, elementText, type ElementText } from './selectorUtils';
|
import { type TextMatcher, elementMatchesText, elementText, type ElementText } from './selectorUtils';
|
||||||
import { SelectorEvaluatorImpl } from './selectorEvaluator';
|
import { SelectorEvaluatorImpl, sortInDOMOrder } from './selectorEvaluator';
|
||||||
import { enclosingShadowRootOrDocument, isElementVisible, parentElementOrShadowHost } from './domUtils';
|
import { enclosingShadowRootOrDocument, isElementVisible, parentElementOrShadowHost } from './domUtils';
|
||||||
import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser';
|
import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser';
|
||||||
import { generateSelector } from './selectorGenerator';
|
import { generateSelector } from './selectorGenerator';
|
||||||
|
|
@ -113,6 +113,7 @@ export class InjectedScript {
|
||||||
this._engines.set('visible', this._createVisibleEngine());
|
this._engines.set('visible', this._createVisibleEngine());
|
||||||
this._engines.set('internal:control', this._createControlEngine());
|
this._engines.set('internal:control', this._createControlEngine());
|
||||||
this._engines.set('internal:has', this._createHasEngine());
|
this._engines.set('internal:has', this._createHasEngine());
|
||||||
|
this._engines.set('internal:or', { queryAll: () => [] });
|
||||||
this._engines.set('internal:label', this._createInternalLabelEngine());
|
this._engines.set('internal:label', this._createInternalLabelEngine());
|
||||||
this._engines.set('internal:text', this._createTextEngine(true, true));
|
this._engines.set('internal:text', this._createTextEngine(true, true));
|
||||||
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
|
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
|
||||||
|
|
@ -169,6 +170,14 @@ export class InjectedScript {
|
||||||
return new Set<Element>(list.slice(nth, nth + 1));
|
return new Set<Element>(list.slice(nth, nth + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _queryOr(elements: Set<Element>, part: ParsedSelectorPart): Set<Element> {
|
||||||
|
const list = [...elements];
|
||||||
|
let nth = +part.body;
|
||||||
|
if (nth === -1)
|
||||||
|
nth = list.length - 1;
|
||||||
|
return new Set<Element>(list.slice(nth, nth + 1));
|
||||||
|
}
|
||||||
|
|
||||||
private _queryLayoutSelector(elements: Set<Element>, part: ParsedSelectorPart, originalRoot: Node): Set<Element> {
|
private _queryLayoutSelector(elements: Set<Element>, part: ParsedSelectorPart, originalRoot: Node): Set<Element> {
|
||||||
const name = part.name as LayoutSelectorName;
|
const name = part.name as LayoutSelectorName;
|
||||||
const body = part.body as NestedSelectorBody;
|
const body = part.body as NestedSelectorBody;
|
||||||
|
|
@ -210,6 +219,9 @@ export class InjectedScript {
|
||||||
for (const part of selector.parts) {
|
for (const part of selector.parts) {
|
||||||
if (part.name === 'nth') {
|
if (part.name === 'nth') {
|
||||||
roots = this._queryNth(roots, part);
|
roots = this._queryNth(roots, part);
|
||||||
|
} 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 (kLayoutSelectorNames.includes(part.name as LayoutSelectorName)) {
|
} else if (kLayoutSelectorNames.includes(part.name as LayoutSelectorName)) {
|
||||||
roots = this._queryLayoutSelector(roots, part, root);
|
roots = this._queryLayoutSelector(roots, part, root);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -518,7 +518,7 @@ function previousSiblingInContext(element: Element, context: QueryContext): Elem
|
||||||
return element.previousElementSibling || undefined;
|
return element.previousElementSibling || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortInDOMOrder(elements: Element[]): Element[] {
|
export function sortInDOMOrder(elements: Iterable<Element>): Element[] {
|
||||||
type SortEntry = { children: Element[], taken: boolean };
|
type SortEntry = { children: Element[], taken: boolean };
|
||||||
|
|
||||||
const elementToEntry = new Map<Element, SortEntry>();
|
const elementToEntry = new Map<Element, SortEntry>();
|
||||||
|
|
@ -540,7 +540,8 @@ function sortInDOMOrder(elements: Element[]): Element[] {
|
||||||
elementToEntry.set(element, entry);
|
elementToEntry.set(element, entry);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
elements.forEach(e => append(e).taken = true);
|
for (const e of elements)
|
||||||
|
append(e).taken = true;
|
||||||
|
|
||||||
function visit(element: Element) {
|
function visit(element: Element) {
|
||||||
const entry = elementToEntry.get(element)!;
|
const entry = elementToEntry.get(element)!;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export class Selectors {
|
||||||
'data-testid', 'data-testid:light',
|
'data-testid', 'data-testid:light',
|
||||||
'data-test-id', 'data-test-id:light',
|
'data-test-id', 'data-test-id:light',
|
||||||
'data-test', 'data-test:light',
|
'data-test', 'data-test:light',
|
||||||
'nth', 'visible', 'internal:control', 'internal:has', 'internal:has-text',
|
'nth', 'visible', 'internal:control', 'internal:has', 'internal:has-text', 'internal:or',
|
||||||
'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role', 'internal:testid',
|
'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role', 'internal:testid',
|
||||||
]);
|
]);
|
||||||
this._builtinEnginesInMainWorld = new Set([
|
this._builtinEnginesInMainWorld = new Set([
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringi
|
||||||
import type { ParsedSelector } from './selectorParser';
|
import type { ParsedSelector } from './selectorParser';
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
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';
|
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has' | 'frame' | 'or';
|
||||||
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||||
|
|
||||||
type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp };
|
type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp };
|
||||||
|
|
@ -86,6 +86,11 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||||
tokens.push(factory.generateLocator(base, 'has', inner));
|
tokens.push(factory.generateLocator(base, 'has', inner));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (part.name === 'internal:or') {
|
||||||
|
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||||
|
tokens.push(factory.generateLocator(base, 'or', inner));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (part.name === 'internal:label') {
|
if (part.name === 'internal:label') {
|
||||||
const { exact, text } = detectExact(part.body as string);
|
const { exact, text } = detectExact(part.body as string);
|
||||||
tokens.push(factory.generateLocator(base, 'label', text, { exact }));
|
tokens.push(factory.generateLocator(base, 'label', text, { exact }));
|
||||||
|
|
@ -195,6 +200,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||||
return `filter({ hasText: ${this.toHasText(body as string)} })`;
|
return `filter({ hasText: ${this.toHasText(body as string)} })`;
|
||||||
case 'has':
|
case 'has':
|
||||||
return `filter({ has: ${body} })`;
|
return `filter({ has: ${body} })`;
|
||||||
|
case 'or':
|
||||||
|
return `or(${body})`;
|
||||||
case 'test-id':
|
case 'test-id':
|
||||||
return `getByTestId(${this.quote(body as string)})`;
|
return `getByTestId(${this.quote(body as string)})`;
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|
@ -263,6 +270,8 @@ export class PythonLocatorFactory implements LocatorFactory {
|
||||||
return `filter(has_text=${this.toHasText(body as string)})`;
|
return `filter(has_text=${this.toHasText(body as string)})`;
|
||||||
case 'has':
|
case 'has':
|
||||||
return `filter(has=${body})`;
|
return `filter(has=${body})`;
|
||||||
|
case 'or':
|
||||||
|
return `or_(${body})`;
|
||||||
case 'test-id':
|
case 'test-id':
|
||||||
return `get_by_test_id(${this.quote(body as string)})`;
|
return `get_by_test_id(${this.quote(body as string)})`;
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|
@ -340,6 +349,8 @@ export class JavaLocatorFactory implements LocatorFactory {
|
||||||
return `filter(new ${clazz}.FilterOptions().setHasText(${this.toHasText(body)}))`;
|
return `filter(new ${clazz}.FilterOptions().setHasText(${this.toHasText(body)}))`;
|
||||||
case 'has':
|
case 'has':
|
||||||
return `filter(new ${clazz}.FilterOptions().setHas(${body}))`;
|
return `filter(new ${clazz}.FilterOptions().setHas(${body}))`;
|
||||||
|
case 'or':
|
||||||
|
return `or(${body})`;
|
||||||
case 'test-id':
|
case 'test-id':
|
||||||
return `getByTestId(${this.quote(body as string)})`;
|
return `getByTestId(${this.quote(body as string)})`;
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|
@ -411,6 +422,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||||
return `Filter(new() { ${this.toHasText(body)} })`;
|
return `Filter(new() { ${this.toHasText(body)} })`;
|
||||||
case 'has':
|
case 'has':
|
||||||
return `Filter(new() { Has = ${body} })`;
|
return `Filter(new() { Has = ${body} })`;
|
||||||
|
case 'or':
|
||||||
|
return `Or(${body})`;
|
||||||
case 'test-id':
|
case 'test-id':
|
||||||
return `GetByTestId(${this.quote(body as string)})`;
|
return `GetByTestId(${this.quote(body as string)})`;
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ function parseLocator(locator: string, testIdAttributeName: string): string {
|
||||||
.replace(/new\(\)/g, '')
|
.replace(/new\(\)/g, '')
|
||||||
.replace(/new[\w]+\.[\w]+options\(\)/g, '')
|
.replace(/new[\w]+\.[\w]+options\(\)/g, '')
|
||||||
.replace(/\.set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase())
|
.replace(/\.set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase())
|
||||||
|
.replace(/\._or\(/g, 'or(') // Python has "_or" instead of "or".
|
||||||
.replace(/:/g, '=')
|
.replace(/:/g, '=')
|
||||||
.replace(/,re\.ignorecase/g, 'i')
|
.replace(/,re\.ignorecase/g, 'i')
|
||||||
.replace(/,pattern.case_insensitive/g, 'i')
|
.replace(/,pattern.case_insensitive/g, 'i')
|
||||||
|
|
@ -101,6 +102,7 @@ function shiftParams(template: string, sub: number) {
|
||||||
|
|
||||||
function transform(template: string, params: TemplateParams, testIdAttributeName: string): string {
|
function transform(template: string, params: TemplateParams, testIdAttributeName: string): string {
|
||||||
// Recursively handle filter(has=).
|
// Recursively handle filter(has=).
|
||||||
|
// TODO: handle or(locator) as well.
|
||||||
while (true) {
|
while (true) {
|
||||||
const hasMatch = template.match(/filter\(,?has=/);
|
const hasMatch = template.match(/filter\(,?has=/);
|
||||||
if (!hasMatch)
|
if (!hasMatch)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { InvalidSelectorError, parseCSS } from './cssParser';
|
||||||
export { InvalidSelectorError, isInvalidSelectorError } from './cssParser';
|
export { InvalidSelectorError, isInvalidSelectorError } from './cssParser';
|
||||||
|
|
||||||
export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number };
|
export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number };
|
||||||
const kNestedSelectorNames = new Set(['internal:has', 'left-of', 'right-of', 'above', 'below', 'near']);
|
const kNestedSelectorNames = new Set(['internal:has', 'internal:or', 'left-of', 'right-of', 'above', 'below', 'near']);
|
||||||
const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);
|
const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);
|
||||||
|
|
||||||
export type ParsedSelectorPart = {
|
export type ParsedSelectorPart = {
|
||||||
|
|
|
||||||
17
packages/playwright-core/types/types.d.ts
vendored
17
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -11446,6 +11446,23 @@ export interface Locator {
|
||||||
*/
|
*/
|
||||||
nth(index: number): Locator;
|
nth(index: number): Locator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a locator that matches either of the two locators.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* If your page shows a username input that is labelled either `Username` or `Login`, depending on some external
|
||||||
|
* factors you do not control, you can match both.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* const input = page.getByLabel('Username').or(page.getByLabel('Login'));
|
||||||
|
* await input.fill('John');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param locator Alternative locator to match.
|
||||||
|
*/
|
||||||
|
or(locator: Locator): Locator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A page this locator belongs to.
|
* A page this locator belongs to.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,11 @@ it('should support playwright.locator({ has })', async ({ page }) => {
|
||||||
expect(await page.evaluate(`playwright.locator('div', { has: playwright.locator('text=Hello') }).element.innerHTML`)).toContain('span');
|
expect(await page.evaluate(`playwright.locator('div', { has: playwright.locator('text=Hello') }).element.innerHTML`)).toContain('span');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support playwright.or()', async ({ page }) => {
|
||||||
|
await page.setContent('<div>Hi</div><span>Hello</span>');
|
||||||
|
expect(await page.evaluate(`playwright.locator('div').or(playwright.locator('span')).elements.map(e => e.innerHTML)`)).toEqual(['Hi', 'Hello']);
|
||||||
|
});
|
||||||
|
|
||||||
it('should support playwright.getBy*', async ({ page }) => {
|
it('should support playwright.getBy*', async ({ page }) => {
|
||||||
await page.setContent('<span>Hello</span><span title="world">World</span>');
|
await page.setContent('<span>Hello</span><span title="world">World</span>');
|
||||||
expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello');
|
expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello');
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,13 @@ it.describe(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('asLocator internal:or', async () => {
|
||||||
|
expect.soft(asLocator('javascript', 'div >> internal:or="span >> article"', false)).toBe(`locator('div').or(locator('span').locator('article'))`);
|
||||||
|
expect.soft(asLocator('python', 'div >> internal:or="span >> article"', false)).toBe(`locator("div").or_(locator("span").locator("article"))`);
|
||||||
|
expect.soft(asLocator('java', 'div >> internal:or="span >> article"', false)).toBe(`locator("div").or(locator("span").locator("article"))`);
|
||||||
|
expect.soft(asLocator('csharp', 'div >> internal:or="span >> article"', false)).toBe(`Locator("div").Or(Locator("span").Locator("article"))`);
|
||||||
|
});
|
||||||
|
|
||||||
it('parse locators strictly', () => {
|
it('parse locators strictly', () => {
|
||||||
const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span';
|
const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,18 @@ it('should support locator.filter', async ({ page, trace }) => {
|
||||||
})).toHaveCount(1);
|
})).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support locator.or', async ({ page }) => {
|
||||||
|
await page.setContent(`<div>hello</div><span>world</span>`);
|
||||||
|
await expect(page.locator('div').or(page.locator('span'))).toHaveCount(2);
|
||||||
|
await expect(page.locator('div').or(page.locator('span'))).toHaveText(['hello', 'world']);
|
||||||
|
await expect(page.locator('span').or(page.locator('article')).or(page.locator('div'))).toHaveText(['hello', 'world']);
|
||||||
|
await expect(page.locator('article').or(page.locator('someting'))).toHaveCount(0);
|
||||||
|
await expect(page.locator('article').or(page.locator('div'))).toHaveText('hello');
|
||||||
|
await expect(page.locator('article').or(page.locator('span'))).toHaveText('world');
|
||||||
|
await expect(page.locator('div').or(page.locator('article'))).toHaveText('hello');
|
||||||
|
await expect(page.locator('span').or(page.locator('article'))).toHaveText('world');
|
||||||
|
});
|
||||||
|
|
||||||
it('should enforce same frame for has/leftOf/rightOf/above/below/near', async ({ page, server }) => {
|
it('should enforce same frame for has/leftOf/rightOf/above/below/near', async ({ page, server }) => {
|
||||||
await page.goto(server.PREFIX + '/frames/two-frames.html');
|
await page.goto(server.PREFIX + '/frames/two-frames.html');
|
||||||
const child = page.frames()[1];
|
const child = page.frames()[1];
|
||||||
|
|
|
||||||
|
|
@ -400,6 +400,20 @@ it('should work with internal:has=', async ({ page, server }) => {
|
||||||
expect(error4.message).toContain('Unexpected token "!" while parsing selector "span!"');
|
expect(error4.message).toContain('Unexpected token "!" while parsing selector "span!"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should work with internal:or=', async ({ page, server }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<div>hello</div>
|
||||||
|
<span>world</span>
|
||||||
|
`);
|
||||||
|
expect(await page.$$eval(`div >> internal:or="span"`, els => els.map(e => e.textContent))).toEqual(['hello', 'world']);
|
||||||
|
expect(await page.$$eval(`span >> internal:or="div"`, els => els.map(e => e.textContent))).toEqual(['hello', 'world']);
|
||||||
|
expect(await page.$$eval(`article >> internal:or="something"`, els => els.length)).toBe(0);
|
||||||
|
expect(await page.locator(`article >> internal:or="div"`).textContent()).toBe('hello');
|
||||||
|
expect(await page.locator(`article >> internal:or="span"`).textContent()).toBe('world');
|
||||||
|
expect(await page.locator(`div >> internal:or="article"`).textContent()).toBe('hello');
|
||||||
|
expect(await page.locator(`span >> internal:or="article"`).textContent()).toBe('world');
|
||||||
|
});
|
||||||
|
|
||||||
it('chaining should work with large DOM @smoke', async ({ page, server }) => {
|
it('chaining should work with large DOM @smoke', async ({ page, server }) => {
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
let last = document.body;
|
let last = document.body;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue