ItemLocator.java

package org.vaadin.addons.dramafinder.element.utils;

import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.AriaRole;

/**
 * Utility for locating a single item inside a container — an option in a
 * {@code vaadin-combo-box} overlay, a tab in a {@code vaadin-tabs}, a checkbox
 * in a {@code vaadin-checkbox-group}, a menu item in a menu — by the item's
 * full label.
 * <p>
 * Every lookup here matches the label <strong>exactly</strong> (case-sensitive,
 * whitespace-normalized) and returns an unfiltered locator, i.e. no
 * {@code .first()}. Both properties are deliberate: Playwright's default
 * accessible-name and text matching is a case-insensitive <em>substring</em>
 * match, so {@code "Option 1"} also matches {@code "Option 10"}, and a trailing
 * {@code .first()} would silently resolve that ambiguity in DOM order. In a test
 * library that is worse than a misdirected click — an assertion keyed on the same
 * lookup passes against the wrong element. Without {@code .first()}, a genuine
 * duplicate fails loudly with a Playwright strict-mode error instead.
 *
 * @see AccessibleNameLocator for the substring-matching lookups used by the
 *      page-level field factories
 */
public final class ItemLocator {

    private ItemLocator() {
    }

    /**
     * Locator for elements whose own visible text is exactly {@code text}.
     * <p>
     * Exact text matching compares against an element's <em>immediate</em> text
     * nodes, so a container also holding nested items is not matched by one of
     * its children's labels.
     *
     * @param page the Playwright page
     * @param text the full visible text to match
     * @return a locator for every element with exactly that text
     */
    public static Locator exactText(Page page, String text) {
        return page.getByText(text, new Page.GetByTextOptions().setExact(true));
    }

    /**
     * Find items of the given tag name inside a container by their exact
     * visible text.
     *
     * @param scope       the container to search within
     * @param itemTagName the item's web component tag name
     * @param label       the item's full label
     * @return a locator for the matching item(s); strict mode fails on duplicates
     */
    public static Locator byExactText(Locator scope, String itemTagName, String label) {
        return scope.locator(itemTagName)
                .filter(new Locator.FilterOptions().setHas(exactText(scope.page(), label)));
    }

    /**
     * Find items of the given tag name inside a container by the item's
     * <em>own</em> exact visible text.
     * <p>
     * Unlike {@link #byExactText(Locator, String, String)} this matches only the
     * text nodes the item holds directly, ignoring the text of any nested items.
     * That is what containers whose items can contain other items of the same
     * kind — {@code vaadin-side-nav-item}, for instance — need: matching the whole
     * subtree would make a parent read as {@code "Parent Child 1 Child 2"} and
     * never match its own label. The flip side is that an item whose label is
     * wrapped in an element rather than sitting in a text node is not matched, so
     * prefer {@link #byExactText(Locator, String, String)} for flat lists of
     * items that may render arbitrary content.
     *
     * @param scope       the container to search within
     * @param itemTagName the item's web component tag name
     * @param label       the item's own full label
     * @return a locator for the matching item(s); strict mode fails on duplicates
     */
    public static Locator byOwnExactText(Locator scope, String itemTagName, String label) {
        return scope.locator(itemTagName + ":text-is(" + cssString(label) + ")");
    }

    private static String cssString(String value) {
        return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
    }

    /**
     * Find items inside a container by their exact accessible name and ARIA role.
     *
     * @param scope the container to search within
     * @param role  the ARIA role of the item
     * @param name  the item's full accessible name
     * @return a locator for the matching item(s); strict mode fails on duplicates
     */
    public static Locator byExactName(Locator scope, AriaRole role, String name) {
        return scope.getByRole(role,
                new Locator.GetByRoleOptions().setName(name).setExact(true));
    }
}