AppLayoutElement.java
package org.vaadin.addons.dramafinder.element;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.BoundingBox;
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-app-layout>}.
* <p>
* Exposes the three areas of an application shell — navbar, drawer and content —
* as locators over the light DOM slotted into them, plus helpers to read and
* change the drawer state through the {@code <vaadin-drawer-toggle>} placed in
* the navbar.
* <p>
* Drawer state is read from the reflected {@code drawer-opened} attribute and
* the layout mode from the reflected {@code overlay} attribute rather than from
* measured geometry: the drawer animates open and closed, so positions and
* sizes are transient while a transition runs.
* <p>
* In overlay mode (narrow viewports) an opened drawer is covered by a backdrop
* that spans the whole viewport, so the drawer toggle cannot be clicked while
* the drawer is open. Use {@link #closeDrawer()}, {@link #clickBackdrop()} or
* {@link #closeDrawerWithEscape()} to close it.
*/
@PlaywrightElement(AppLayoutElement.FIELD_TAG_NAME)
public class AppLayoutElement extends VaadinElement implements HasStyleElement, HasThemeElement {
public static final String FIELD_TAG_NAME = "vaadin-app-layout";
/** Tag name of the drawer toggle button placed in the navbar. */
public static final String DRAWER_TOGGLE_TAG_NAME = "vaadin-drawer-toggle";
/** Value of the {@code primary-section} attribute when the navbar comes first. */
public static final String PRIMARY_SECTION_NAVBAR = "navbar";
/** Value of the {@code primary-section} attribute when the drawer comes first. */
public static final String PRIMARY_SECTION_DRAWER = "drawer";
/**
* Create a new {@code AppLayoutElement}.
*
* @param locator the locator for the {@code <vaadin-app-layout>} element
*/
public AppLayoutElement(Locator locator) {
super(locator);
}
/**
* Get the first {@code AppLayoutElement} on the page.
*
* @param page the Playwright page
* @return the first matching {@code AppLayoutElement}
*/
public static AppLayoutElement get(Page page) {
return new AppLayoutElement(page.locator(FIELD_TAG_NAME).first());
}
/**
* Get the first {@code AppLayoutElement} within a parent locator.
*
* @param parent the parent locator to search within
* @return the first matching {@code AppLayoutElement}
*/
public static AppLayoutElement get(Locator parent) {
return new AppLayoutElement(parent.locator(FIELD_TAG_NAME).first());
}
/**
* Get an {@code AppLayoutElement} by its {@code id} attribute.
*
* @param page the Playwright page
* @param id the element id
* @return the matching {@code AppLayoutElement}
*/
public static AppLayoutElement getById(Page page, String id) {
return new AppLayoutElement(page.locator("#" + id));
}
// ── Areas ─────────────────────────────────────────────────────────
/**
* Locator for the content slotted into the top navbar.
* <p>
* Matches every child assigned to the {@code navbar} slot, so it may resolve
* to more than one element; chain further locators to target a single one.
*
* @return the locator for the navbar content
*/
public Locator getNavbarLocator() {
return getLocator().locator("> [slot~='navbar']");
}
/**
* Locator for the content slotted into the bottom navbar.
* <p>
* Components added as touch-optimized are moved to this slot by the
* component itself, and only on touchscreen devices; on other devices the
* locator resolves to nothing.
*
* @return the locator for the bottom navbar content
*/
public Locator getBottomNavbarLocator() {
return getLocator().locator("> [slot='navbar-bottom']");
}
/**
* Locator for the content slotted into the drawer.
* <p>
* Matches every child assigned to the {@code drawer} slot, so it may resolve
* to more than one element; chain further locators to target a single one.
*
* @return the locator for the drawer content
*/
public Locator getDrawerLocator() {
return getLocator().locator("> [slot='drawer']");
}
/**
* Locator for the content area, that is, the children without a slot.
* <p>
* With Flow this is the component set as the layout content, typically the
* currently routed view.
*
* @return the locator for the content area
*/
public Locator getContentLocator() {
// using xpath to not pierce the shadow dom
return getLocator().locator("xpath=./*[not(@slot)]");
}
/**
* Locator for the backdrop shown behind an opened drawer in overlay mode.
*
* @return the locator for the {@code backdrop} part
*/
public Locator getBackdropLocator() {
return getLocator().locator("[part='backdrop']");
}
// ── Drawer toggle ─────────────────────────────────────────────────
/**
* Get the drawer toggle button of this layout.
*
* @return the {@code <vaadin-drawer-toggle>} as a {@code ButtonElement}
*/
public ButtonElement getDrawerToggle() {
return new ButtonElement(getLocator().locator(DRAWER_TOGGLE_TAG_NAME).first());
}
/**
* Click the drawer toggle, flipping the drawer state.
* <p>
* In overlay mode the backdrop of an opened drawer covers the navbar and
* therefore the toggle; prefer {@link #openDrawer()} and
* {@link #closeDrawer()}, which pick a usable target for the current mode.
*/
public void toggleDrawer() {
getDrawerToggle().click();
}
// ── Drawer state ──────────────────────────────────────────────────
/**
* Whether the drawer is opened.
*
* @return {@code true} when the {@code drawer-opened} attribute is present
*/
public boolean isDrawerOpened() {
return getLocator().getAttribute("drawer-opened") != null;
}
/**
* Open the drawer by clicking the drawer toggle, unless it is already opened.
*/
public void openDrawer() {
if (!isDrawerOpened()) {
toggleDrawer();
}
assertDrawerOpened();
}
/**
* Close the drawer, unless it is already closed.
* <p>
* Presses {@code Escape} in overlay mode, where the backdrop covers the
* drawer toggle, and clicks the drawer toggle otherwise.
*/
public void closeDrawer() {
if (isDrawerOpened()) {
if (isOverlayMode()) {
closeDrawerWithEscape();
} else {
toggleDrawer();
}
}
assertDrawerClosed();
}
/**
* Set the drawer state, opening or closing it as needed.
*
* @param opened {@code true} to open the drawer, {@code false} to close it
*/
public void setDrawerOpened(boolean opened) {
if (opened) {
openDrawer();
} else {
closeDrawer();
}
}
/**
* Click the backdrop, which closes the drawer in overlay mode.
* <p>
* The backdrop spans the whole viewport, including the part of it covered by
* the opened drawer, so the click is aimed at the side of the backdrop the
* drawer leaves free. The backdrop only receives pointer events while the
* drawer is opened in overlay mode; clicking it in any other state fails.
*/
public void clickBackdrop() {
Locator backdrop = getBackdropLocator();
BoundingBox backdropBox = backdrop.boundingBox();
BoundingBox drawerBox = getLocator().locator("[part='drawer']").boundingBox();
if (backdropBox == null || drawerBox == null) {
backdrop.click();
return;
}
double spaceBeforeDrawer = drawerBox.x - backdropBox.x;
double spaceAfterDrawer = backdropBox.x + backdropBox.width - (drawerBox.x + drawerBox.width);
double x = spaceAfterDrawer >= spaceBeforeDrawer ? backdropBox.width - 1 : 1;
backdrop.click(new Locator.ClickOptions()
.setPosition(x, backdropBox.height / 2));
}
/**
* Press {@code Escape}, which closes the drawer in overlay mode.
*/
public void closeDrawerWithEscape() {
getLocator().page().keyboard().press("Escape");
}
/**
* Assert that the drawer is opened.
*/
public void assertDrawerOpened() {
assertThat(getLocator()).hasAttribute("drawer-opened", "");
}
/**
* Assert that the drawer is closed.
*/
public void assertDrawerClosed() {
assertThat(getLocator()).not().hasAttribute("drawer-opened", "");
}
// ── Layout mode ───────────────────────────────────────────────────
/**
* Whether the drawer is displayed as an overlay on top of the content,
* which the component enables on small viewports.
*
* @return {@code true} when the {@code overlay} attribute is present
*/
public boolean isOverlayMode() {
return getLocator().getAttribute("overlay") != null;
}
/**
* Assert that the layout is in overlay mode.
*/
public void assertOverlayMode() {
assertThat(getLocator()).hasAttribute("overlay", "");
}
/**
* Assert that the layout is not in overlay mode.
*/
public void assertNotOverlayMode() {
assertThat(getLocator()).not().hasAttribute("overlay", "");
}
/**
* Get the primary section, that is, the area that comes first visually.
*
* @return {@link #PRIMARY_SECTION_NAVBAR} or {@link #PRIMARY_SECTION_DRAWER}
*/
public String getPrimarySection() {
return getLocator().getAttribute("primary-section");
}
/**
* Assert the primary section of the layout.
*
* @param primarySection the expected value, {@link #PRIMARY_SECTION_NAVBAR}
* or {@link #PRIMARY_SECTION_DRAWER}
*/
public void assertPrimarySection(String primarySection) {
assertThat(getLocator()).hasAttribute("primary-section", primarySection);
}
// ── Presence of the areas ─────────────────────────────────────────
/**
* Assert that the layout has content slotted into the drawer.
*/
public void assertHasDrawer() {
assertThat(getLocator()).hasAttribute("has-drawer", "");
}
/**
* Assert that the layout has content slotted into the navbar.
*/
public void assertHasNavbar() {
assertThat(getLocator()).hasAttribute("has-navbar", "");
}
}