feat(locator): remove locator.and and locator.not (#22223)

Not shipping for now, after API review.
This commit is contained in:
Dmitry Gozman 2023-04-05 16:28:13 -07:00 committed by GitHub
parent b5195122d9
commit 08cef43e82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 4 additions and 283 deletions

View File

@ -98,44 +98,6 @@ String[] texts = page.getByRole(AriaRole.LINK).allTextContents();
var texts = await page.GetByRole(AriaRole.Link).AllTextContentsAsync(); 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 ## async method: Locator.blur
* since: v1.28 * 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-%% ### option: Locator.locator.hasNotText = %%-locator-option-has-not-text-%%
* since: v1.33 * 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 ## method: Locator.nth
* since: v1.14 * since: v1.14

View File

@ -1056,54 +1056,6 @@ await Expect(page
Note that the inner locator is matched starting from the outer one, not from the document root. 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 ## 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. 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.

View File

@ -192,12 +192,6 @@ export class Locator implements api.Locator {
return this._frame.$$(this._selector); 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 { first(): Locator {
return new Locator(this._frame, this._selector + ' >> nth=0'); 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}`); 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 { or(locator: Locator): Locator {
if (locator._frame !== this._frame) if (locator._frame !== this._frame)
throw new Error(`Locators must belong to the same frame.`); throw new Error(`Locators must belong to the same frame.`);

View File

@ -61,9 +61,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.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.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.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.and;
delete this._injectedScript.window.playwright.or; delete this._injectedScript.window.playwright.or;
delete this._injectedScript.window.playwright.not;
} }
private _querySelector(selector: string, strict: boolean): (Element | undefined) { private _querySelector(selector: string, strict: boolean): (Element | undefined) {

View File

@ -114,8 +114,6 @@ export class InjectedScript {
this._engines.set('internal:has', this._createHasEngine()); this._engines.set('internal:has', this._createHasEngine());
this._engines.set('internal:has-not', this._createHasNotEngine()); this._engines.set('internal:has-not', this._createHasNotEngine());
this._engines.set('internal:or', { queryAll: () => [] }); 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: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());
@ -217,12 +215,6 @@ export class InjectedScript {
} else if (part.name === 'internal:or') { } else if (part.name === 'internal:or') {
const orElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root); const orElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root);
roots = new Set(sortInDOMOrder(new Set([...roots, ...orElements]))); 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)) { } else if (kLayoutSelectorNames.includes(part.name as LayoutSelectorName)) {
roots = this._queryLayoutSelector(roots, part, root); roots = this._queryLayoutSelector(roots, part, root);
} else { } else {

View File

@ -38,7 +38,7 @@ export class Selectors {
'nth', 'visible', 'internal:control', 'nth', 'visible', 'internal:control',
'internal:has', 'internal:has-not', 'internal:has', 'internal:has-not',
'internal:has-text', 'internal:has-not-text', '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', 'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role', 'internal:testid',
]); ]);
this._builtinEnginesInMainWorld = new Set([ this._builtinEnginesInMainWorld = new Set([

View File

@ -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-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'; 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 };
@ -104,16 +104,6 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
tokens.push(factory.generateLocator(base, 'or', inner)); tokens.push(factory.generateLocator(base, 'or', inner));
continue; 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') { 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 }));
@ -229,10 +219,6 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
return `filter({ hasNot: ${body} })`; return `filter({ hasNot: ${body} })`;
case 'or': case 'or':
return `or(${body})`; return `or(${body})`;
case 'and':
return `and(${body})`;
case 'not':
return `not(${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':
@ -307,10 +293,6 @@ export class PythonLocatorFactory implements LocatorFactory {
return `filter(has_not=${body})`; return `filter(has_not=${body})`;
case 'or': case 'or':
return `or_(${body})`; return `or_(${body})`;
case 'and':
return `and_(${body})`;
case 'not':
return `not_(${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':
@ -394,10 +376,6 @@ export class JavaLocatorFactory implements LocatorFactory {
return `filter(new ${clazz}.FilterOptions().setHasNot(${body}))`; return `filter(new ${clazz}.FilterOptions().setHasNot(${body}))`;
case 'or': case 'or':
return `or(${body})`; return `or(${body})`;
case 'and':
return `and(${body})`;
case 'not':
return `not(${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':
@ -475,10 +453,6 @@ export class CSharpLocatorFactory implements LocatorFactory {
return `Filter(new() { HasNot = ${body} })`; return `Filter(new() { HasNot = ${body} })`;
case 'or': case 'or':
return `Or(${body})`; return `Or(${body})`;
case 'and':
return `And(${body})`;
case 'not':
return `Not(${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':

View File

@ -80,7 +80,6 @@ function parseLocator(locator: string, testIdAttributeName: string): string {
.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(/\.or_\(/g, 'or(') // Python has "or_" instead of "or".
.replace(/\.not_\(/g, 'not(') // Python has "not_" instead of "not".
.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')
@ -105,7 +104,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=, hasnot=). // Recursively handle filter(has=, hasnot=).
// TODO: handle or(locator), not(locator), and(locator). // TODO: handle or(locator).
while (true) { while (true) {
const hasMatch = template.match(/filter\(,?(has|hasnot)=/); const hasMatch = template.match(/filter\(,?(has|hasnot)=/);
if (!hasMatch) if (!hasMatch)

View File

@ -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', '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']); const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);
export type ParsedSelectorPart = { export type ParsedSelectorPart = {

View File

@ -10240,21 +10240,6 @@ export interface Locator {
*/ */
allTextContents(): Promise<Array<string>>; 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. * Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) on the element.
* @param options * @param options
@ -11539,21 +11524,6 @@ export interface Locator {
hasText?: string|RegExp; hasText?: string|RegExp;
}): Locator; }): 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. * Returns locator to the n-th matching element. It's zero based, `nth(0)` selects the first element.
* *

View File

@ -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']); 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 }) => { 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');

View File

@ -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"))`); 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', () => { it('parse locators strictly', () => {
const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span'; const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span';

View File

@ -176,30 +176,6 @@ it('should support locator.or', async ({ page }) => {
await expect(page.locator('span').or(page.locator('article'))).toHaveText('world'); await expect(page.locator('span').or(page.locator('article'))).toHaveText('world');
}); });
it('should support locator.and', async ({ page }) => {
await page.setContent(`
<div data-testid=foo>hello</div><div data-testid=bar>world</div>
<span data-testid=foo>hello2</span><span data-testid=bar>world2</span>
`);
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(`<div class=foo>hello</div><div class=bar>world</div>`);
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 }) => { 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];

View File

@ -423,32 +423,6 @@ it('should work with internal:or=', async ({ page, server }) => {
expect(await page.locator(`span >> internal:or="article"`).textContent()).toBe('world'); expect(await page.locator(`span >> internal:or="article"`).textContent()).toBe('world');
}); });
it('should work with internal:and=', async ({ page, server }) => {
await page.setContent(`
<div class=foo>hello</div><div class=bar>world</div>
<span class=foo>hello2</span><span class=bar>world2</span>
`);
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(`
<div class=foo>hello</div>
<div class=bar>world</div>
`);
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 }) => { 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;