BreadcrumbsElement.java

package org.vaadin.addons.dramafinder.element;

import java.util.List;

import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.AriaRole;
import org.vaadin.addons.dramafinder.element.shared.HasAriaLabelElement;
import org.vaadin.addons.dramafinder.element.shared.HasStyleElement;
import org.vaadin.addons.dramafinder.element.shared.HasThemeElement;

import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

/**
 * PlaywrightElement for {@code <vaadin-breadcrumbs>}.
 * <p>
 * The trail is made of {@code <vaadin-breadcrumbs-item>} light-DOM children,
 * exposed as {@link BreadcrumbsItemElement}. When the trail does not fit, the
 * component sets the {@code has-overflow} attribute, reveals an
 * {@code [part='overflow-button']} and moves the collapsed items to
 * {@code slot="overlay"}; those items stay children of the host, so
 * {@link #getItems()} covers both the inline and the overflowing set.
 * <p>
 * The component is rendered with the ARIA role {@code navigation}, which
 * {@link #getByLabel(Page, String)} uses for lookup.
 */
@PlaywrightElement(BreadcrumbsElement.FIELD_TAG_NAME)
public class BreadcrumbsElement extends VaadinElement
        implements HasAriaLabelElement, HasStyleElement, HasThemeElement {

    public static final String FIELD_TAG_NAME = "vaadin-breadcrumbs";

    private static final String OVERFLOW_BUTTON_PART = "[part='overflow-button']";

    /**
     * Create a new {@code BreadcrumbsElement}.
     *
     * @param locator the locator for the {@code <vaadin-breadcrumbs>} element
     */
    public BreadcrumbsElement(Locator locator) {
        super(locator);
    }

    // --- Item access ---

    /**
     * Locator for every {@code <vaadin-breadcrumbs-item>} of the trail,
     * including the items collapsed into the overflow overlay.
     *
     * @return locator matching all items of the trail
     */
    public Locator getItemsLocator() {
        return getLocator().locator(BreadcrumbsItemElement.FIELD_TAG_NAME);
    }

    /**
     * Get every item of the trail, in trail order, including the items
     * collapsed into the overflow overlay.
     *
     * @return the items of the trail, empty when there are none
     */
    public List<BreadcrumbsItemElement> getItems() {
        return getItemsLocator().all().stream().map(BreadcrumbsItemElement::new).toList();
    }

    /**
     * Get the item at the given position in the trail.
     *
     * @param index zero-based index
     * @return the item at that index
     */
    public BreadcrumbsItemElement getItem(int index) {
        return new BreadcrumbsItemElement(getItemsLocator().nth(index));
    }

    /**
     * Get the item showing the given text. The text is matched in full, so
     * {@code "Products"} does not match an item reading {@code "Products archive"}.
     *
     * @param text the exact text of the item
     * @return the first matching item; its locator matches nothing when no item
     *         has that text
     */
    public BreadcrumbsItemElement getItem(String text) {
        return new BreadcrumbsItemElement(getItemsLocator()
                .and(getLocator().getByText(text, new Locator.GetByTextOptions().setExact(true)))
                .first());
    }

    /**
     * Get the item marked as the current page, that is, the last item of the
     * trail when it has no path.
     *
     * @return the current item; its locator matches nothing when the trail has
     *         no current item
     */
    public BreadcrumbsItemElement getCurrentItem() {
        return new BreadcrumbsItemElement(getLocator()
                .locator(BreadcrumbsItemElement.FIELD_TAG_NAME + "[current]").first());
    }

    /**
     * Assert that the trail holds exactly the expected number of items.
     *
     * @param count the expected item count
     */
    public void assertItemCount(int count) {
        assertThat(getItemsLocator()).hasCount(count);
    }

    /**
     * Assert that the trail shows exactly the given texts, in trail order.
     *
     * @param texts the expected item texts
     */
    public void assertItemTexts(String... texts) {
        assertThat(getItemsLocator()).hasText(texts);
    }

    // --- Overflow ---

    /**
     * Locator for the button that reveals the collapsed items. The button is
     * hidden unless the trail overflows.
     *
     * @return locator for the overflow button
     */
    public Locator getOverflowButtonLocator() {
        return getLocator().locator(OVERFLOW_BUTTON_PART);
    }

    /**
     * Whether one or more items are collapsed into the overflow overlay.
     *
     * @return {@code true} when the {@code has-overflow} attribute is set
     */
    public boolean hasOverflow() {
        return getLocator().getAttribute("has-overflow") != null;
    }

    /**
     * Assert that items are collapsed into the overflow overlay.
     */
    public void assertHasOverflow() {
        assertThat(getLocator()).hasAttribute("has-overflow", "");
    }

    /**
     * Assert that the whole trail fits and nothing is collapsed.
     */
    public void assertHasNoOverflow() {
        assertThat(getLocator()).not().hasAttribute("has-overflow", "");
    }

    /**
     * Locator for the items collapsed into the overflow overlay.
     *
     * @return locator matching the overflowing items, empty when the trail fits
     */
    public Locator getOverflowItemsLocator() {
        return getLocator().locator(
                BreadcrumbsItemElement.FIELD_TAG_NAME + "[slot='overlay']");
    }

    /**
     * Get the items collapsed into the overflow overlay, in trail order.
     *
     * @return the overflowing items, empty when the trail fits
     */
    public List<BreadcrumbsItemElement> getOverflowItems() {
        return getOverflowItemsLocator().all().stream().map(BreadcrumbsItemElement::new).toList();
    }

    /**
     * Whether the overflow overlay is open.
     *
     * @return {@code true} when the overflow button reports {@code aria-expanded="true"}
     */
    public boolean isOverflowOpen() {
        return "true".equals(getOverflowButtonLocator().getAttribute("aria-expanded"));
    }

    /**
     * Open the overflow overlay, revealing the collapsed items. Does nothing
     * when the overlay is already open.
     */
    public void openOverflow() {
        if (!isOverflowOpen()) {
            getOverflowButtonLocator().click();
        }
    }

    /**
     * Close the overflow overlay by pressing {@code Escape}. Does nothing when
     * the overlay is already closed.
     * <p>
     * The open overlay captures pointer events, so the overlay cannot be closed
     * by clicking the overflow button a second time.
     */
    public void closeOverflow() {
        if (isOverflowOpen()) {
            getLocator().page().keyboard().press("Escape");
        }
    }

    /**
     * Assert that the overflow overlay is open.
     */
    public void assertOverflowOpen() {
        assertThat(getOverflowButtonLocator()).hasAttribute("aria-expanded", "true");
    }

    /**
     * Assert that the overflow overlay is closed.
     */
    public void assertOverflowClosed() {
        assertThat(getOverflowButtonLocator()).hasAttribute("aria-expanded", "false");
    }

    /**
     * Assert the accessible name of the overflow button, which comes from
     * {@code BreadcrumbsI18n.moreItems}.
     *
     * @param label the expected accessible name
     */
    public void assertOverflowButtonAriaLabel(String label) {
        assertThat(getOverflowButtonLocator()).hasAttribute("aria-label", label);
    }

    // --- Factory methods ---

    /**
     * Get the first {@code <vaadin-breadcrumbs>} on the page.
     *
     * @param page the Playwright page
     * @return the first {@code BreadcrumbsElement}
     */
    public static BreadcrumbsElement get(Page page) {
        return new BreadcrumbsElement(page.locator(FIELD_TAG_NAME).first());
    }

    /**
     * Get the first {@code <vaadin-breadcrumbs>} within a locator scope.
     *
     * @param locator the scope to search within
     * @return the first {@code BreadcrumbsElement}
     */
    public static BreadcrumbsElement get(Locator locator) {
        return new BreadcrumbsElement(locator.locator(FIELD_TAG_NAME).first());
    }

    /**
     * Get the {@code <vaadin-breadcrumbs>} by its accessible name, using the
     * ARIA role {@code navigation}.
     *
     * @param page  the Playwright page
     * @param label the accessible label of the breadcrumbs
     * @return the matching {@code BreadcrumbsElement}
     */
    public static BreadcrumbsElement getByLabel(Page page, String label) {
        return new BreadcrumbsElement(
                page.getByRole(AriaRole.NAVIGATION, new Page.GetByRoleOptions().setName(label))
                        .and(page.locator(FIELD_TAG_NAME)).first());
    }
}