mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(locator): filter({ hasNot }) (#22219)
This is the opposite of `filter({ has })`.
This commit is contained in:
parent
8dd431745d
commit
bc1de5f28d
@ -1345,6 +1345,9 @@ Returns whether the element is [visible](../actionability.md#visible). [`option:
|
||||
### option: Frame.locator.-inline- = %%-locator-options-list-v1.14-%%
|
||||
* since: v1.14
|
||||
|
||||
### option: Frame.locator.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
## method: Frame.name
|
||||
* since: v1.8
|
||||
- returns: <[string]>
|
||||
|
@ -202,6 +202,9 @@ Returns locator to the last matching frame.
|
||||
### option: FrameLocator.locator.-inline- = %%-locator-options-list-v1.14-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: FrameLocator.locator.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
## method: FrameLocator.nth
|
||||
* since: v1.17
|
||||
- returns: <[FrameLocator]>
|
||||
|
@ -988,6 +988,8 @@ await rowLocator
|
||||
### option: Locator.filter.-inline- = %%-locator-options-list-v1.14-%%
|
||||
* since: v1.22
|
||||
|
||||
### option: Locator.filter.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
## method: Locator.first
|
||||
* since: v1.14
|
||||
@ -1503,6 +1505,9 @@ var banana = await page.GetByRole(AriaRole.Listitem).Last(1);
|
||||
### option: Locator.locator.-inline- = %%-locator-options-list-v1.14-%%
|
||||
* since: v1.14
|
||||
|
||||
### option: Locator.locator.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
|
||||
## method: Locator.not
|
||||
* since: v1.33
|
||||
|
@ -2684,6 +2684,9 @@ Returns whether the element is [visible](../actionability.md#visible). [`option:
|
||||
### option: Page.locator.-inline- = %%-locator-options-list-v1.14-%%
|
||||
* since: v1.14
|
||||
|
||||
### option: Page.locator.hasNot = %%-locator-option-has-not-%%
|
||||
* since: v1.33
|
||||
|
||||
## method: Page.mainFrame
|
||||
* since: v1.8
|
||||
- returns: <[Frame]>
|
||||
|
@ -1029,6 +1029,14 @@ For example, `article` that has `text=Playwright` matches `<article><div>Playwri
|
||||
|
||||
Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s.
|
||||
|
||||
## locator-option-has-not
|
||||
- `hasNot` <[Locator]>
|
||||
|
||||
Matches elements that do not contain an element that matches an inner locator. Inner locator is queried against the outer one.
|
||||
For example, `article` that does not have `div` matches `<article><span>Playwright</span></article>`.
|
||||
|
||||
Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s.
|
||||
|
||||
## locator-options-list-v1.14
|
||||
- %%-locator-option-has-text-%%
|
||||
- %%-locator-option-has-%%
|
||||
|
@ -885,7 +885,7 @@ await page
|
||||
|
||||
### 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.
|
||||
Locators support an option to only select elements that have or have not 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.
|
||||
|
||||
```html card
|
||||
<ul>
|
||||
@ -983,6 +983,47 @@ await Expect(page
|
||||
.toHaveCountAsync(1);
|
||||
```
|
||||
|
||||
We can also filter by **not having** a matching element inside
|
||||
|
||||
```js
|
||||
await expect(page
|
||||
.getByRole('listitem')
|
||||
.filter({ hasNot: page.getByText('Product 2') }))
|
||||
.toHaveCount(1);
|
||||
```
|
||||
|
||||
```java
|
||||
assertThat(page
|
||||
.getByRole(AriaRole.LISTITEM)
|
||||
.filter(new Locator.FilterOptions().setHasNot(page.getByText("Product 2")))
|
||||
.hasCount(1);
|
||||
```
|
||||
|
||||
```python async
|
||||
await expect(
|
||||
page.get_by_role("listitem").filter(
|
||||
has_not=page.get_by_role("heading", name="Product 2")
|
||||
)
|
||||
).to_have_count(1)
|
||||
```
|
||||
|
||||
```python sync
|
||||
expect(
|
||||
page.get_by_role("listitem").filter(
|
||||
has_not=page.get_by_role("heading", name="Product 2")
|
||||
)
|
||||
).to_have_count(1)
|
||||
```
|
||||
|
||||
```csharp
|
||||
await Expect(page
|
||||
.GetByRole(AriaRole.Listitem)
|
||||
.Filter(new() {
|
||||
HasNot = page.GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
|
||||
})
|
||||
.toHaveCountAsync(1);
|
||||
```
|
||||
|
||||
Note that the inner locator is matched starting from the outer one, not from the document root.
|
||||
|
||||
### Filter by matching an additional locator
|
||||
|
@ -30,6 +30,7 @@ import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, get
|
||||
export type LocatorOptions = {
|
||||
hasText?: string | RegExp;
|
||||
has?: Locator;
|
||||
hasNot?: Locator;
|
||||
};
|
||||
|
||||
export class Locator implements api.Locator {
|
||||
@ -49,6 +50,13 @@ export class Locator implements api.Locator {
|
||||
throw new Error(`Inner "has" locator must belong to the same frame.`);
|
||||
this._selector += ` >> internal:has=` + JSON.stringify(locator._selector);
|
||||
}
|
||||
|
||||
if (options?.hasNot) {
|
||||
const locator = options.hasNot;
|
||||
if (locator._frame !== frame)
|
||||
throw new Error(`Inner "hasNot" locator must belong to the same frame.`);
|
||||
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
|
||||
}
|
||||
}
|
||||
|
||||
private async _withElement<R>(task: (handle: ElementHandle<SVGElement | HTMLElement>, timeout?: number) => Promise<R>, timeout?: number): Promise<R> {
|
||||
|
@ -29,13 +29,15 @@ class Locator {
|
||||
element: Element | undefined;
|
||||
elements: Element[] | undefined;
|
||||
|
||||
constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, has?: Locator }) {
|
||||
constructor(injectedScript: InjectedScript, selector: string, options?: { hasText?: string | RegExp, has?: Locator, hasNot?: Locator }) {
|
||||
(this as any)[selectorSymbol] = selector;
|
||||
(this as any)[injectedScriptSymbol] = injectedScript;
|
||||
if (options?.hasText)
|
||||
selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
|
||||
if (options?.has)
|
||||
selector += ` >> internal:has=` + JSON.stringify((options.has as any)[selectorSymbol]);
|
||||
if (options?.hasNot)
|
||||
selector += ` >> internal:has-not=` + JSON.stringify((options.hasNot as any)[selectorSymbol]);
|
||||
if (selector) {
|
||||
const parsed = injectedScript.parseSelector(selector);
|
||||
this.element = injectedScript.querySelector(parsed, injectedScript.document, false);
|
||||
|
@ -112,6 +112,7 @@ export class InjectedScript {
|
||||
this._engines.set('visible', this._createVisibleEngine());
|
||||
this._engines.set('internal:control', this._createControlEngine());
|
||||
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: () => [] });
|
||||
@ -377,6 +378,16 @@ export class InjectedScript {
|
||||
return { queryAll };
|
||||
}
|
||||
|
||||
private _createHasNotEngine(): SelectorEngine {
|
||||
const queryAll = (root: SelectorRoot, body: NestedSelectorBody) => {
|
||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||
return [];
|
||||
const has = !!this.querySelector(body.parsed, root, false);
|
||||
return has ? [] : [root as Element];
|
||||
};
|
||||
return { queryAll };
|
||||
}
|
||||
|
||||
private _createVisibleEngine(): SelectorEngine {
|
||||
const queryAll = (root: SelectorRoot, body: string) => {
|
||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||
|
@ -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',
|
||||
'nth', 'visible', 'internal:control', 'internal:has', 'internal:has-not', 'internal:has-text',
|
||||
'internal:or', 'internal:and', 'internal:not',
|
||||
'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role', 'internal:testid',
|
||||
]);
|
||||
|
@ -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' | 'and' | 'not';
|
||||
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has' | 'hasNot' | 'frame' | 'or' | 'and' | 'not';
|
||||
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||
|
||||
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));
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:has-not') {
|
||||
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||
tokens.push(factory.generateLocator(base, 'hasNot', inner));
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:or') {
|
||||
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||
tokens.push(factory.generateLocator(base, 'or', inner));
|
||||
@ -210,6 +215,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||
return `filter({ hasText: ${this.toHasText(body as string)} })`;
|
||||
case 'has':
|
||||
return `filter({ has: ${body} })`;
|
||||
case 'hasNot':
|
||||
return `filter({ hasNot: ${body} })`;
|
||||
case 'or':
|
||||
return `or(${body})`;
|
||||
case 'and':
|
||||
@ -284,6 +291,8 @@ export class PythonLocatorFactory implements LocatorFactory {
|
||||
return `filter(has_text=${this.toHasText(body as string)})`;
|
||||
case 'has':
|
||||
return `filter(has=${body})`;
|
||||
case 'hasNot':
|
||||
return `filter(has_not=${body})`;
|
||||
case 'or':
|
||||
return `or_(${body})`;
|
||||
case 'and':
|
||||
@ -367,6 +376,8 @@ export class JavaLocatorFactory implements LocatorFactory {
|
||||
return `filter(new ${clazz}.FilterOptions().setHasText(${this.toHasText(body)}))`;
|
||||
case 'has':
|
||||
return `filter(new ${clazz}.FilterOptions().setHas(${body}))`;
|
||||
case 'hasNot':
|
||||
return `filter(new ${clazz}.FilterOptions().setHasNot(${body}))`;
|
||||
case 'or':
|
||||
return `or(${body})`;
|
||||
case 'and':
|
||||
@ -444,6 +455,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||
return `Filter(new() { ${this.toHasText(body)} })`;
|
||||
case 'has':
|
||||
return `Filter(new() { Has = ${body} })`;
|
||||
case 'hasNot':
|
||||
return `Filter(new() { HasNot = ${body} })`;
|
||||
case 'or':
|
||||
return `Or(${body})`;
|
||||
case 'and':
|
||||
|
@ -72,6 +72,7 @@ function parseLocator(locator: string, testIdAttributeName: string): string {
|
||||
.replace(/get_by_test_id/g, 'getbytestid')
|
||||
.replace(/get_by_([\w]+)/g, 'getby$1')
|
||||
.replace(/has_text/g, 'hastext')
|
||||
.replace(/has_not/g, 'hasnot')
|
||||
.replace(/frame_locator/g, 'framelocator')
|
||||
.replace(/[{}\s]/g, '')
|
||||
.replace(/new\(\)/g, '')
|
||||
@ -102,10 +103,10 @@ function shiftParams(template: string, sub: number) {
|
||||
}
|
||||
|
||||
function transform(template: string, params: TemplateParams, testIdAttributeName: string): string {
|
||||
// Recursively handle filter(has=).
|
||||
// Recursively handle filter(has=, hasnot=).
|
||||
// TODO: handle or(locator), not(locator), and(locator).
|
||||
while (true) {
|
||||
const hasMatch = template.match(/filter\(,?has=/);
|
||||
const hasMatch = template.match(/filter\(,?(has|hasnot)=/);
|
||||
if (!hasMatch)
|
||||
break;
|
||||
|
||||
@ -129,6 +130,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
||||
const hasSelector = JSON.stringify(transform(hasTemplate, hasParams, testIdAttributeName));
|
||||
|
||||
// Replace filter(has=...) with filter(has2=$5). Use has2 to avoid matching the same filter again.
|
||||
// Replace filter(hasnot=...) with filter(hasnot2=$5). Use hasnot2 to avoid matching the same filter again.
|
||||
template = template.substring(0, start - 1) + `2=$${paramsCountBeforeHas + 1}` + shiftParams(template.substring(end), paramsCountInHas - 1);
|
||||
|
||||
// Replace inner params with $5 value.
|
||||
@ -151,6 +153,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
||||
.replace(/nth\(([^)]+)\)/g, 'nth=$1')
|
||||
.replace(/filter\(,?hastext=([^)]+)\)/g, 'internal:has-text=$1')
|
||||
.replace(/filter\(,?has2=([^)]+)\)/g, 'internal:has=$1')
|
||||
.replace(/filter\(,?hasnot2=([^)]+)\)/g, 'internal:has-not=$1')
|
||||
.replace(/,exact=false/g, '')
|
||||
.replace(/,exact=true/g, 's')
|
||||
.replace(/\,/g, '][');
|
||||
@ -180,7 +183,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
||||
})
|
||||
.replace(/\$(\d+)(i|s)?/g, (_, ordinal, suffix) => {
|
||||
const param = params[+ordinal - 1];
|
||||
if (t.startsWith('internal:has='))
|
||||
if (t.startsWith('internal:has=') || t.startsWith('internal:has-not='))
|
||||
return param.text;
|
||||
if (t.startsWith('internal:attr') || t.startsWith('internal:testid') || t.startsWith('internal:role'))
|
||||
return escapeForAttributeSelector(param.text, suffix === 's');
|
||||
|
@ -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', 'internal:and', 'internal:not', 'left-of', 'right-of', 'above', 'below', 'near']);
|
||||
const kNestedSelectorNames = new Set(['internal:has', 'internal:has-not', 'internal:or', 'internal:and', 'internal:not', 'left-of', 'right-of', 'above', 'below', 'near']);
|
||||
const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']);
|
||||
|
||||
export type ParsedSelectorPart = {
|
||||
|
40
packages/playwright-core/types/types.d.ts
vendored
40
packages/playwright-core/types/types.d.ts
vendored
@ -3217,6 +3217,14 @@ export interface Page {
|
||||
*/
|
||||
has?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain an element that matches an inner locator. Inner locator is queried against the
|
||||
* outer one. For example, `article` that does not have `div` matches `<article><span>Playwright</span></article>`.
|
||||
*
|
||||
* Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s.
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
@ -6594,6 +6602,14 @@ export interface Frame {
|
||||
*/
|
||||
has?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain an element that matches an inner locator. Inner locator is queried against the
|
||||
* outer one. For example, `article` that does not have `div` matches `<article><span>Playwright</span></article>`.
|
||||
*
|
||||
* Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s.
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
@ -10827,6 +10843,14 @@ export interface Locator {
|
||||
*/
|
||||
has?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain an element that matches an inner locator. Inner locator is queried against the
|
||||
* outer one. For example, `article` that does not have `div` matches `<article><span>Playwright</span></article>`.
|
||||
*
|
||||
* Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s.
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
@ -11475,6 +11499,14 @@ export interface Locator {
|
||||
*/
|
||||
has?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain an element that matches an inner locator. Inner locator is queried against the
|
||||
* outer one. For example, `article` that does not have `div` matches `<article><span>Playwright</span></article>`.
|
||||
*
|
||||
* Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s.
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
@ -17125,6 +17157,14 @@ export interface FrameLocator {
|
||||
*/
|
||||
has?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements that do not contain an element that matches an inner locator. Inner locator is queried against the
|
||||
* outer one. For example, `article` that does not have `div` matches `<article><span>Playwright</span></article>`.
|
||||
*
|
||||
* Note that outer and inner locators must belong to the same frame. Inner locator must not contain [FrameLocator]s.
|
||||
*/
|
||||
hasNot?: Locator;
|
||||
|
||||
/**
|
||||
* Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. When
|
||||
* passed a [string], matching is case-insensitive and searches for a substring. For example, `"Playwright"` matches
|
||||
|
@ -67,6 +67,12 @@ 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.locator({ hasNot })', async ({ page }) => {
|
||||
await page.setContent('<div>Hi</div><div><span>Hello</span></div>');
|
||||
expect(await page.evaluate(`playwright.locator('div', { hasNot: playwright.locator('span') }).element.innerHTML`)).toContain('Hi');
|
||||
expect(await page.evaluate(`playwright.locator('div', { hasNot: playwright.locator('text=Hello') }).element.innerHTML`)).toContain('Hi');
|
||||
});
|
||||
|
||||
it('should support locator.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']);
|
||||
|
@ -312,6 +312,27 @@ it('reverse engineer has', async ({ page }) => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reverse engineer hasNot', async ({ page }) => {
|
||||
expect.soft(generate(page.getByText('Hello').filter({ hasNot: page.locator('div').getByText('bye') }))).toEqual({
|
||||
csharp: `GetByText("Hello").Filter(new() { HasNot = Locator("div").GetByText("bye") })`,
|
||||
java: `getByText("Hello").filter(new Locator.FilterOptions().setHasNot(locator("div").getByText("bye")))`,
|
||||
javascript: `getByText('Hello').filter({ hasNot: locator('div').getByText('bye') })`,
|
||||
python: `get_by_text("Hello").filter(has_not=locator("div").get_by_text("bye"))`,
|
||||
});
|
||||
|
||||
const locator = page
|
||||
.locator('section')
|
||||
.filter({ has: page.locator('div').filter({ hasNot: page.locator('span') }) })
|
||||
.filter({ hasText: 'foo' })
|
||||
.filter({ hasNot: page.locator('a') });
|
||||
expect.soft(generate(locator)).toEqual({
|
||||
csharp: `Locator("section").Filter(new() { Has = Locator("div").Filter(new() { HasNot = Locator("span") }) }).Filter(new() { HasText = "foo" }).Filter(new() { HasNot = Locator("a") })`,
|
||||
java: `locator("section").filter(new Locator.FilterOptions().setHas(locator("div").filter(new Locator.FilterOptions().setHasNot(locator("span"))))).filter(new Locator.FilterOptions().setHasText("foo")).filter(new Locator.FilterOptions().setHasNot(locator("a")))`,
|
||||
javascript: `locator('section').filter({ has: locator('div').filter({ hasNot: locator('span') }) }).filter({ hasText: 'foo' }).filter({ hasNot: locator('a') })`,
|
||||
python: `locator("section").filter(has=locator("div").filter(has_not=locator("span"))).filter(has_text="foo").filter(has_not=locator("a"))`,
|
||||
});
|
||||
});
|
||||
|
||||
it('reverse engineer frameLocator', async ({ page }) => {
|
||||
const locator = page
|
||||
.frameLocator('iframe')
|
||||
|
@ -157,6 +157,9 @@ it('should support locator.filter', async ({ page, trace }) => {
|
||||
has: page.locator('span'),
|
||||
hasText: 'world',
|
||||
})).toHaveCount(1);
|
||||
await expect(page.locator(`div`).filter({ hasNot: page.locator('span', { hasText: 'world' }) })).toHaveCount(1);
|
||||
await expect(page.locator(`div`).filter({ hasNot: page.locator('section') })).toHaveCount(2);
|
||||
await expect(page.locator(`div`).filter({ hasNot: page.locator('span') })).toHaveCount(0);
|
||||
});
|
||||
|
||||
it('should support locator.or', async ({ page }) => {
|
||||
|
@ -400,6 +400,15 @@ it('should work with internal:has=', async ({ page, server }) => {
|
||||
expect(error4.message).toContain('Unexpected token "!" while parsing selector "span!"');
|
||||
});
|
||||
|
||||
it('should work with internal:has-not=', async ({ page }) => {
|
||||
await page.setContent(`<section><span></span><div></div></section><section><br></section>`);
|
||||
expect(await page.$$eval(`section >> internal:has-not="span"`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`section >> internal:has-not="span, div, br"`, els => els.length)).toBe(0);
|
||||
expect(await page.$$eval(`section >> internal:has-not="br"`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`section >> internal:has-not="span, div"`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`section >> internal:has-not="article"`, els => els.length)).toBe(2);
|
||||
});
|
||||
|
||||
it('should work with internal:or=', async ({ page, server }) => {
|
||||
await page.setContent(`
|
||||
<div>hello</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user