microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
66.69k stars 3.65k forks source link

[Feature] Advanced text selection & cursor positioning methods #22873

Closed segevfiner closed 11 months ago

segevfiner commented 1 year ago

Playwright currently has Locator.selectText, but this only allows selecting all the text of the node. For some kinds of components, e.g. Advanced input boxes, WYSIWYG editors, and so on. It would be useful to be able to select ranges of text by position, regexp, and so on; and also to be able to be able to position the cursor.

You currently have to implement this yourself via evaluate. See Stack Overflow - How can I select part of the sentence in playwright? for a simple example.

segevfiner commented 1 year ago

I wrote the following as a workaround:

import type { Locator } from '@playwright/test';

/**
 * Select text range by RegExp.
 *
 * @param locator Element locator
 * @param pattern The pattern to match
 * @param flags RegExp flags
 */
export async function selectTextRe(locator: Locator, pattern: string | RegExp, flags?: string): Promise<void> {
  await locator.evaluate(
    (element, { pattern, flags }) => {
      const textNode = element.childNodes[0];
      const match = textNode.textContent?.match(new RegExp(pattern, flags));
      if (match) {
        const range = document.createRange();
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        range.setStart(textNode, match.index!);
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        range.setEnd(textNode, match.index! + match[0].length);
        const selection = document.getSelection();
        selection?.removeAllRanges();
        selection?.addRange(range);
      }
    },
    { pattern, flags }
  );
}

/**
 * Set cursor position after RegExp match.
 *
 * @param locator Element locator
 * @param pattern The pattern to match
 * @param flags RegExp flags
 */
export async function setCursorAfterRe(locator: Locator, pattern: string | RegExp, flags?: string): Promise<void> {
  await locator.evaluate(
    (element, { pattern, flags }) => {
      const textNode = element.childNodes[0];
      const match = textNode.textContent?.match(new RegExp(pattern, flags));
      if (match) {
        const selection = document.getSelection();
        selection?.removeAllRanges();
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        selection?.setPosition(textNode, match.index! + match[0].length);
      }
    },
    { pattern, flags }
  );
}
Dinh246 commented 1 year ago

did research this too and ended up with the function like this for click to highlight text, you can highlight by using keyboard press shift+Arrow too hope it helps

async clickToBlackedText({ selector, index, triple }: BlackedText) {
    const blockSelector = this.frameLocator.locator(selector);
    await blockSelector.click();
    await this.selectOptionOnQuickBar("Edit text");
    await this.frameLocator.locator("[contenteditable=true]").waitFor();
    for (let i = 0; i < index; i++) {
      await this.page.keyboard.press("ArrowRight");
    }
    const rect = await blockSelector.evaluate(() => {
      const wbRect = window.parent.document.querySelector("#preview").getBoundingClientRect();
      const range = document.getSelection().getRangeAt(0).cloneRange();
      const eleRect = range.getClientRects()[0];
      return { x: wbRect.x + eleRect.x, y: wbRect.y + eleRect.y };
    });
    await this.page.mouse.move(rect.x, rect.y);
    await this.page.mouse.dblclick(rect.x, rect.y);
    if (triple) {
      await this.page.mouse.click(rect.x, rect.y);
    }

I found that the code below will find exactly the position of your cursor editor (in contenteditable)

const rect = await blockSelector.evaluate(() => {
      const wbRect = window.parent.document.querySelector("#preview").getBoundingClientRect();
      const range = document.getSelection().getRangeAt(0).cloneRange();
      const eleRect = range.getClientRects()[0];
      return { x: wbRect.x + eleRect.x, y: wbRect.y + eleRect.y };
    });

Let me explain a bit, since the cursor is in a frame so to get the exact position I have to add eleRect with wbRect (the frame top/left) if yours is not in any frame so just do

const range = document.getSelection().getRangeAt(0).cloneRange();
const eleRect = range.getClientRects()[0];

and return x, y

pavelfeldman commented 11 months ago

Why was this issue closed?

Thank you for your involvement. This issue was closed due to limited engagement (upvotes/activity), lack of recent activity, and insufficient actionability. To maintain a manageable database, we prioritize issues based on these factors.

If you disagree with this closure, please open a new issue and reference this one. More support or clarity on its necessity may prompt a review. Your understanding and cooperation are appreciated.

raimohanska commented 7 months ago

Upvote! This is my top grief with Playwright, which is otherwise awesome.

Being able to select text (all or a certain part) and possibly placing the caret in a given position would be awesome.

4rtemOv commented 6 months ago

Looking forward to see this feature