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)')
|
||||
```
|
||||
|
||||
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 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';
|
||||
}
|
||||
|
||||
function boxRightOf(box1: DOMRect, box2: DOMRect): number | undefined {
|
||||
if (box1.left < box2.right)
|
||||
function boxRightOf(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||
const distance = box1.left - box2.right;
|
||||
if (distance < 0 || (maxDistance !== undefined && distance > maxDistance))
|
||||
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 {
|
||||
if (box1.right > box2.left)
|
||||
function boxLeftOf(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||
const distance = box2.left - box1.right;
|
||||
if (distance < 0 || (maxDistance !== undefined && distance > maxDistance))
|
||||
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 {
|
||||
if (box1.bottom > box2.top)
|
||||
function boxAbove(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||
const distance = box2.top - box1.bottom;
|
||||
if (distance < 0 || (maxDistance !== undefined && distance > maxDistance))
|
||||
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 {
|
||||
if (box1.top < box2.bottom)
|
||||
function boxBelow(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||
const distance = box1.top - box2.bottom;
|
||||
if (distance < 0 || (maxDistance !== undefined && distance > maxDistance))
|
||||
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 {
|
||||
const kThreshold = 50;
|
||||
function boxNear(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined {
|
||||
const kThreshold = maxDistance === undefined ? 50 : maxDistance;
|
||||
let score = 0;
|
||||
if (box1.left - box2.right >= 0)
|
||||
score += box1.left - box2.right;
|
||||
@ -574,17 +578,19 @@ function boxNear(box1: DOMRect, box2: DOMRect): number | undefined {
|
||||
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 {
|
||||
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
||||
if (!args.length)
|
||||
throw new Error(`"${name}" engine expects a selector list`);
|
||||
const maxDistance = args.length && typeof args[args.length - 1] === 'number' ? args[args.length - 1] : undefined;
|
||||
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();
|
||||
let bestScore: number | undefined;
|
||||
for (const e of evaluator.query(context, args)) {
|
||||
for (const e of evaluator.query(context, queryArgs)) {
|
||||
if (e === element)
|
||||
continue;
|
||||
const score = scorer(box, e.getBoundingClientRect());
|
||||
const score = scorer(box, e.getBoundingClientRect(), maxDistance);
|
||||
if (score === undefined)
|
||||
continue;
|
||||
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(#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, 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.$('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(#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, 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(#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.$('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, 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(#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.$('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, 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(#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(#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)', 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');
|
||||
|
||||
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 }) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user