mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(selectors): support max distance in layout selectors (#6172)
Supports `div:near(button, 120)` with configurable distance in pixels.
This commit is contained in:
parent
82e8c7226d
commit
b62a436041
@ -523,6 +523,9 @@ page.fill('input:right-of(:text("Username"))', 'value')
|
|||||||
page.click('button:near(.promo-card)')
|
page.click('button:near(.promo-card)')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
All layout selectors support optional maximum pixel distance as the last argument. For example
|
||||||
|
`button:near(:text("Username"), 120)` matches a button that is at most 120 pixels away from the element with the text "Username".
|
||||||
|
|
||||||
## XPath selectors
|
## XPath selectors
|
||||||
|
|
||||||
XPath selectors are equivalent to calling [`Document.evaluate`](https://developer.mozilla.org/en/docs/Web/API/Document/evaluate).
|
XPath selectors are equivalent to calling [`Document.evaluate`](https://developer.mozilla.org/en/docs/Web/API/Document/evaluate).
|
||||||
|
@ -536,32 +536,36 @@ export function elementMatchesText(evaluator: SelectorEvaluatorImpl, element: El
|
|||||||
return 'self';
|
return 'self';
|
||||||
}
|
}
|
||||||
|
|
||||||
function boxRightOf(box1: DOMRect, box2: DOMRect): number | undefined {
|
function boxRightOf(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||||
if (box1.left < box2.right)
|
const distance = box1.left - box2.right;
|
||||||
|
if (distance < 0 || (maxDistance !== undefined && distance > maxDistance))
|
||||||
return;
|
return;
|
||||||
return (box1.left - box2.right) + Math.max(box2.bottom - box1.bottom, 0) + Math.max(box1.top - box2.top, 0);
|
return distance + Math.max(box2.bottom - box1.bottom, 0) + Math.max(box1.top - box2.top, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function boxLeftOf(box1: DOMRect, box2: DOMRect): number | undefined {
|
function boxLeftOf(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||||
if (box1.right > box2.left)
|
const distance = box2.left - box1.right;
|
||||||
|
if (distance < 0 || (maxDistance !== undefined && distance > maxDistance))
|
||||||
return;
|
return;
|
||||||
return (box2.left - box1.right) + Math.max(box2.bottom - box1.bottom, 0) + Math.max(box1.top - box2.top, 0);
|
return distance + Math.max(box2.bottom - box1.bottom, 0) + Math.max(box1.top - box2.top, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function boxAbove(box1: DOMRect, box2: DOMRect): number | undefined {
|
function boxAbove(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||||
if (box1.bottom > box2.top)
|
const distance = box2.top - box1.bottom;
|
||||||
|
if (distance < 0 || (maxDistance !== undefined && distance > maxDistance))
|
||||||
return;
|
return;
|
||||||
return (box2.top - box1.bottom) + Math.max(box1.left - box2.left, 0) + Math.max(box2.right - box1.right, 0);
|
return distance + Math.max(box1.left - box2.left, 0) + Math.max(box2.right - box1.right, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function boxBelow(box1: DOMRect, box2: DOMRect): number | undefined {
|
function boxBelow(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||||
if (box1.top < box2.bottom)
|
const distance = box1.top - box2.bottom;
|
||||||
|
if (distance < 0 || (maxDistance !== undefined && distance > maxDistance))
|
||||||
return;
|
return;
|
||||||
return (box1.top - box2.bottom) + Math.max(box1.left - box2.left, 0) + Math.max(box2.right - box1.right, 0);
|
return distance + Math.max(box1.left - box2.left, 0) + Math.max(box2.right - box1.right, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function boxNear(box1: DOMRect, box2: DOMRect): number | undefined {
|
function boxNear(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||||
const kThreshold = 50;
|
const kThreshold = maxDistance === undefined ? 50 : maxDistance;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
if (box1.left - box2.right >= 0)
|
if (box1.left - box2.right >= 0)
|
||||||
score += box1.left - box2.right;
|
score += box1.left - box2.right;
|
||||||
@ -574,17 +578,19 @@ function boxNear(box1: DOMRect, box2: DOMRect): number | undefined {
|
|||||||
return score > kThreshold ? undefined : score;
|
return score > kThreshold ? undefined : score;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPositionEngine(name: string, scorer: (box1: DOMRect, box2: DOMRect) => number | undefined): SelectorEngine {
|
function createPositionEngine(name: string, scorer: (box1: DOMRect, box2: DOMRect, maxDistance: number | undefined) => number | undefined): SelectorEngine {
|
||||||
return {
|
return {
|
||||||
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
||||||
if (!args.length)
|
const maxDistance = args.length && typeof args[args.length - 1] === 'number' ? args[args.length - 1] : undefined;
|
||||||
throw new Error(`"${name}" engine expects a selector list`);
|
const queryArgs = maxDistance === undefined ? args : args.slice(0, args.length - 1);
|
||||||
|
if (args.length < 1 + (maxDistance === undefined ? 0 : 1))
|
||||||
|
throw new Error(`"${name}" engine expects a selector list and optional maximum distance in pixels`);
|
||||||
const box = element.getBoundingClientRect();
|
const box = element.getBoundingClientRect();
|
||||||
let bestScore: number | undefined;
|
let bestScore: number | undefined;
|
||||||
for (const e of evaluator.query(context, args)) {
|
for (const e of evaluator.query(context, queryArgs)) {
|
||||||
if (e === element)
|
if (e === element)
|
||||||
continue;
|
continue;
|
||||||
const score = scorer(box, e.getBoundingClientRect());
|
const score = scorer(box, e.getBoundingClientRect(), maxDistance);
|
||||||
if (score === undefined)
|
if (score === undefined)
|
||||||
continue;
|
continue;
|
||||||
if (bestScore === undefined || score < bestScore)
|
if (bestScore === undefined || score < bestScore)
|
||||||
|
@ -155,6 +155,8 @@ it('should work with position selectors', async ({page}) => {
|
|||||||
expect(await page.$eval('div:right-of(#id0)', e => e.id)).toBe('id7');
|
expect(await page.$eval('div:right-of(#id0)', e => e.id)).toBe('id7');
|
||||||
expect(await page.$eval('div:right-of(#id8)', e => e.id)).toBe('id9');
|
expect(await page.$eval('div:right-of(#id8)', e => e.id)).toBe('id9');
|
||||||
expect(await page.$$eval('div:right-of(#id3)', els => els.map(e => e.id).join(','))).toBe('id4,id2,id5,id7,id8,id9');
|
expect(await page.$$eval('div:right-of(#id3)', els => els.map(e => e.id).join(','))).toBe('id4,id2,id5,id7,id8,id9');
|
||||||
|
expect(await page.$$eval('div:right-of(#id3, 50)', els => els.map(e => e.id).join(','))).toBe('id2,id5,id7,id8');
|
||||||
|
expect(await page.$$eval('div:right-of(#id3, 49)', els => els.map(e => e.id).join(','))).toBe('id7,id8');
|
||||||
|
|
||||||
expect(await page.$eval('div:left-of(#id2)', e => e.id)).toBe('id1');
|
expect(await page.$eval('div:left-of(#id2)', e => e.id)).toBe('id1');
|
||||||
expect(await page.$('div:left-of(#id0)')).toBe(null);
|
expect(await page.$('div:left-of(#id0)')).toBe(null);
|
||||||
@ -162,6 +164,7 @@ it('should work with position selectors', async ({page}) => {
|
|||||||
expect(await page.$eval('div:left-of(#id9)', e => e.id)).toBe('id8');
|
expect(await page.$eval('div:left-of(#id9)', e => e.id)).toBe('id8');
|
||||||
expect(await page.$eval('div:left-of(#id4)', e => e.id)).toBe('id3');
|
expect(await page.$eval('div:left-of(#id4)', e => e.id)).toBe('id3');
|
||||||
expect(await page.$$eval('div:left-of(#id5)', els => els.map(e => e.id).join(','))).toBe('id0,id7,id3,id1,id6,id8');
|
expect(await page.$$eval('div:left-of(#id5)', els => els.map(e => e.id).join(','))).toBe('id0,id7,id3,id1,id6,id8');
|
||||||
|
expect(await page.$$eval('div:left-of(#id5, 3)', els => els.map(e => e.id).join(','))).toBe('id7,id8');
|
||||||
|
|
||||||
expect(await page.$eval('div:above(#id0)', e => e.id)).toBe('id3');
|
expect(await page.$eval('div:above(#id0)', e => e.id)).toBe('id3');
|
||||||
expect(await page.$eval('div:above(#id5)', e => e.id)).toBe('id4');
|
expect(await page.$eval('div:above(#id5)', e => e.id)).toBe('id4');
|
||||||
@ -170,6 +173,7 @@ it('should work with position selectors', async ({page}) => {
|
|||||||
expect(await page.$eval('div:above(#id9)', e => e.id)).toBe('id8');
|
expect(await page.$eval('div:above(#id9)', e => e.id)).toBe('id8');
|
||||||
expect(await page.$('div:above(#id2)')).toBe(null);
|
expect(await page.$('div:above(#id2)')).toBe(null);
|
||||||
expect(await page.$$eval('div:above(#id5)', els => els.map(e => e.id).join(','))).toBe('id4,id2,id3,id1');
|
expect(await page.$$eval('div:above(#id5)', els => els.map(e => e.id).join(','))).toBe('id4,id2,id3,id1');
|
||||||
|
expect(await page.$$eval('div:above(#id5, 20)', els => els.map(e => e.id).join(','))).toBe('id4,id3');
|
||||||
|
|
||||||
expect(await page.$eval('div:below(#id4)', e => e.id)).toBe('id5');
|
expect(await page.$eval('div:below(#id4)', e => e.id)).toBe('id5');
|
||||||
expect(await page.$eval('div:below(#id3)', e => e.id)).toBe('id0');
|
expect(await page.$eval('div:below(#id3)', e => e.id)).toBe('id0');
|
||||||
@ -179,16 +183,22 @@ it('should work with position selectors', async ({page}) => {
|
|||||||
expect(await page.$eval('div:below(#id8)', e => e.id)).toBe('id9');
|
expect(await page.$eval('div:below(#id8)', e => e.id)).toBe('id9');
|
||||||
expect(await page.$('div:below(#id9)')).toBe(null);
|
expect(await page.$('div:below(#id9)')).toBe(null);
|
||||||
expect(await page.$$eval('div:below(#id3)', els => els.map(e => e.id).join(','))).toBe('id0,id5,id6,id7,id8,id9');
|
expect(await page.$$eval('div:below(#id3)', els => els.map(e => e.id).join(','))).toBe('id0,id5,id6,id7,id8,id9');
|
||||||
|
expect(await page.$$eval('div:below(#id3, 105)', els => els.map(e => e.id).join(','))).toBe('id0,id5,id6,id7');
|
||||||
|
|
||||||
expect(await page.$eval('div:near(#id0)', e => e.id)).toBe('id3');
|
expect(await page.$eval('div:near(#id0)', e => e.id)).toBe('id3');
|
||||||
expect(await page.$$eval('div:near(#id7)', els => els.map(e => e.id).join(','))).toBe('id0,id5,id3,id6');
|
expect(await page.$$eval('div:near(#id7)', els => els.map(e => e.id).join(','))).toBe('id0,id5,id3,id6');
|
||||||
expect(await page.$$eval('div:near(#id0)', els => els.map(e => e.id).join(','))).toBe('id3,id6,id7,id8,id1,id5');
|
expect(await page.$$eval('div:near(#id0)', els => els.map(e => e.id).join(','))).toBe('id3,id6,id7,id8,id1,id5');
|
||||||
expect(await page.$$eval('div:near(#id6)', els => els.map(e => e.id).join(','))).toBe('id0,id3,id7');
|
expect(await page.$$eval('div:near(#id6)', els => els.map(e => e.id).join(','))).toBe('id0,id3,id7');
|
||||||
|
expect(await page.$$eval('div:near(#id6, 10)', els => els.map(e => e.id).join(','))).toBe('id0');
|
||||||
|
expect(await page.$$eval('div:near(#id0, 100)', els => els.map(e => e.id).join(','))).toBe('id3,id6,id7,id8,id1,id5,id4,id2');
|
||||||
|
|
||||||
expect(await page.$$eval('div:below(#id5):above(#id8)', els => els.map(e => e.id).join(','))).toBe('id7,id6');
|
expect(await page.$$eval('div:below(#id5):above(#id8)', els => els.map(e => e.id).join(','))).toBe('id7,id6');
|
||||||
expect(await page.$eval('div:below(#id5):above(#id8)', e => e.id)).toBe('id7');
|
expect(await page.$eval('div:below(#id5):above(#id8)', e => e.id)).toBe('id7');
|
||||||
|
|
||||||
expect(await page.$$eval('div:right-of(#id0) + div:above(#id8)', els => els.map(e => e.id).join(','))).toBe('id5,id6,id3');
|
expect(await page.$$eval('div:right-of(#id0) + div:above(#id8)', els => els.map(e => e.id).join(','))).toBe('id5,id6,id3');
|
||||||
|
|
||||||
|
const error = await page.$(':near(50)').catch(e => e);
|
||||||
|
expect(error.message).toContain('"near" engine expects a selector list and optional maximum distance in pixels');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should escape the scope with >>', async ({ page }) => {
|
it('should escape the scope with >>', async ({ page }) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user