diff --git a/docs-src/api-footer.md b/docs-src/api-footer.md index 7b6b293974..287c9e9e28 100644 --- a/docs-src/api-footer.md +++ b/docs-src/api-footer.md @@ -88,7 +88,9 @@ Playwright also supports the following CSS extensions: * `:text("string")` - Matches elements that contain specific text node. Learn more about [text selector](./selectors.md#css-extension-text). * `:visible` - Matches only visible elements. Learn more about [visible selector](./selectors.md#css-extension-visible). * `:light(selector)` - Matches in the light DOM only as opposite to piercing open shadow roots. Learn more about [shadow piercing](./selectors.md#shadow-piercing). + For convenience, selectors in the wrong format are heuristically converted to the right format: - selector starting with `//` or `..` is assumed to be `xpath=selector`; diff --git a/docs/api.md b/docs/api.md index 5b26891402..81a6559e27 100644 --- a/docs/api.md +++ b/docs/api.md @@ -5460,7 +5460,9 @@ Playwright also supports the following CSS extensions: * `:text("string")` - Matches elements that contain specific text node. Learn more about [text selector](./selectors.md#css-extension-text). * `:visible` - Matches only visible elements. Learn more about [visible selector](./selectors.md#css-extension-visible). * `:light(selector)` - Matches in the light DOM only as opposite to piercing open shadow roots. Learn more about [shadow piercing](./selectors.md#shadow-piercing). + For convenience, selectors in the wrong format are heuristically converted to the right format: - selector starting with `//` or `..` is assumed to be `xpath=selector`; diff --git a/docs/selectors.md b/docs/selectors.md index a646f002a8..f41443bf48 100644 --- a/docs/selectors.md +++ b/docs/selectors.md @@ -213,6 +213,7 @@ await page.click('button:text("Sign in")'); await page.click(':light(.article > .header)'); ``` + ### xpath diff --git a/src/server/common/selectorParser.ts b/src/server/common/selectorParser.ts index d1d33f4954..f40c657121 100644 --- a/src/server/common/selectorParser.ts +++ b/src/server/common/selectorParser.ts @@ -35,7 +35,7 @@ export function selectorsV2Enabled() { } export function selectorsV2EngineNames() { - return ['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text-matches', 'text-is', 'above', 'below', 'right-of', 'left-of', 'near', 'within']; + return ['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text-matches', 'text-is']; } export function parseSelector(selector: string, customNames: Set): ParsedSelector { diff --git a/src/server/injected/selectorEvaluator.ts b/src/server/injected/selectorEvaluator.ts index 820144a9fe..2befe53c70 100644 --- a/src/server/injected/selectorEvaluator.ts +++ b/src/server/injected/selectorEvaluator.ts @@ -60,12 +60,6 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator { this._engines.set('xpath', xpathEngine); for (const attr of ['id', 'data-testid', 'data-test-id', 'data-test']) this._engines.set(attr, createAttributeEngine(attr)); - this._engines.set('right-of', createProximityEngine('right-of', boxRightOf)); - this._engines.set('left-of', createProximityEngine('left-of', boxLeftOf)); - this._engines.set('above', createProximityEngine('above', boxAbove)); - this._engines.set('below', createProximityEngine('below', boxBelow)); - this._engines.set('near', createProximityEngine('near', boxNear)); - this._engines.set('within', createProximityEngine('within', boxWithin)); } // This is the only function we should use for querying, because it does @@ -448,70 +442,6 @@ function createAttributeEngine(attr: string): SelectorEngine { }; } -function areCloseRanges(from1: number, to1: number, from2: number, to2: number, threshold: number) { - return to1 >= from2 - threshold && to2 >= from1 - threshold; -} - -function boxSize(box: DOMRect) { - return Math.sqrt(box.width * box.height); -} - -function boxesProximityThreshold(box1: DOMRect, box2: DOMRect) { - return (boxSize(box1) + boxSize(box2)) / 2; -} - -function boxRightOf(box1: DOMRect, box2: DOMRect): boolean { - // To the right, but not too far, and vertically intersects. - const distance = box1.left - box2.right; - return distance >= 0 && distance <= boxesProximityThreshold(box1, box2) && - areCloseRanges(box1.top, box1.bottom, box2.top, box2.bottom, 0); -} - -function boxLeftOf(box1: DOMRect, box2: DOMRect): boolean { - // To the left, but not too far, and vertically intersects. - const distance = box2.left - box1.right; - return distance >= 0 && distance <= boxesProximityThreshold(box1, box2) && - areCloseRanges(box1.top, box1.bottom, box2.top, box2.bottom, 0); -} - -function boxAbove(box1: DOMRect, box2: DOMRect): boolean { - // Above, but not too far, and horizontally intersects. - const distance = box2.top - box1.bottom; - return distance >= 0 && distance <= boxesProximityThreshold(box1, box2) && - areCloseRanges(box1.left, box1.right, box2.left, box2.right, 0); -} - -function boxBelow(box1: DOMRect, box2: DOMRect): boolean { - // Below, but not too far, and horizontally intersects. - const distance = box1.top - box2.bottom; - return distance >= 0 && distance <= boxesProximityThreshold(box1, box2) && - areCloseRanges(box1.left, box1.right, box2.left, box2.right, 0); -} - -function boxWithin(box1: DOMRect, box2: DOMRect): boolean { - return box1.left >= box2.left && box1.right <= box2.right && box1.top >= box2.top && box1.bottom <= box2.bottom; -} - -function boxNear(box1: DOMRect, box2: DOMRect): boolean { - const intersects = !(box1.left >= box2.right || box2.left >= box1.right || box1.top >= box2.bottom || box2.top >= box1.bottom); - if (intersects) - return false; - const threshold = boxesProximityThreshold(box1, box2); - return areCloseRanges(box1.left, box1.right, box2.left, box2.right, threshold) && - areCloseRanges(box1.top, box1.bottom, box2.top, box2.bottom, threshold); -} - -function createProximityEngine(name: string, predicate: (box1: DOMRect, box2: DOMRect) => boolean): 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 box = element.getBoundingClientRect(); - return evaluator.query(context, args).some(e => e !== element && predicate(box, e.getBoundingClientRect())); - }, - }; -} - export function parentElementOrShadowHost(element: Element): Element | undefined { if (element.parentElement) return element.parentElement; diff --git a/test/selectors-misc.spec.ts b/test/selectors-misc.spec.ts index b8c6fd39bf..0d05f93535 100644 --- a/test/selectors-misc.spec.ts +++ b/test/selectors-misc.spec.ts @@ -52,7 +52,9 @@ it('should work with :visible', async ({page}) => { expect(await page.$eval('div:visible', div => div.id)).toBe('target2'); }); -it('should work with proximity selectors', async ({page}) => { +it('should work with proximity selectors', test => { + test.skip('Not ready yet'); +}, async ({page}) => { if (!selectorsV2Enabled()) return; // Selectors v1 do not support this.