diff --git a/TODO.md b/TODO.md index 1037a2d..72f428d 100644 --- a/TODO.md +++ b/TODO.md @@ -4,6 +4,7 @@ - Big Decimal Field - Button +- Card - Checkbox - Date Picker - Date Time Picker @@ -18,6 +19,7 @@ - Progress bar - Radio Button - Select +- Split Layout - Tabs - TextArea - TextField @@ -27,7 +29,6 @@ - App Layout - Avatar -- Card - Combo Box - Confirm Dialog - Context Menu @@ -38,7 +39,6 @@ - Message List - Multi-Select Combobox - Side navigation -- Split Layout - Upload - Virtual List diff --git a/src/main/java/org/vaadin/addons/dramafinder/element/SideNavigationElement.java b/src/main/java/org/vaadin/addons/dramafinder/element/SideNavigationElement.java new file mode 100644 index 0000000..c5a8574 --- /dev/null +++ b/src/main/java/org/vaadin/addons/dramafinder/element/SideNavigationElement.java @@ -0,0 +1,116 @@ +package org.vaadin.addons.dramafinder.element; + +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.options.AriaRole; +import org.vaadin.addons.dramafinder.element.shared.HasLabelElement; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; + +/** + * PlaywrightElement for {@code }. + */ +@PlaywrightElement(SideNavigationElement.FIELD_TAG_NAME) +public class SideNavigationElement extends VaadinElement implements HasLabelElement { + + public static final String FIELD_TAG_NAME = "vaadin-side-nav"; + + public SideNavigationElement(Locator locator) { + super(locator); + } + + @Override + public Locator getLabelLocator() { + return getLocator().locator("> [slot='label']"); + } + + /** + * Checks if the side nav is collapsed. + */ + public boolean isCollapsed() { + return getLocator().getAttribute("collapsed") != null; + } + + /** + * Asserts that the side nav is collapsed. + */ + public void assertCollapsed() { + assertThat(getLocator()).hasAttribute("collapsed", ""); + } + + /** + * Asserts that the side nav is expanded. + */ + public void assertExpanded() { + assertThat(getLocator()).not().hasAttribute("collapsed", ""); + } + + /** + * Asserts that the side nav is collapsible. + */ + public void assertCollapsible() { + assertThat(getLocator()).hasAttribute("collapsible", ""); + } + + /** + * Asserts that the side nav is not collapsible. + */ + public void assertNotCollapsible() { + assertThat(getLocator()).not().hasAttribute("collapsible", ""); + } + + /** + * Gets a SideNavigationItemElement by its label text. + * This searches for a direct or nested vaadin-side-nav-item with the given + * text. + * Note: This strictly searches for the item that contains the text. + * Use care if multiple items have the same text. + * Your navigation item has to be visible, you'll need to open manually the parent. + * + * @param label The label of the item. + * @return The SideNavigationItemElement. + */ + public SideNavigationItemElement getItem(String label) { + // Using locator with hasText might be too broad if parent contains child text. + // But vaadin-side-nav-item encapsulates its content. + // Let's try to match exact text or contains. + // A common strategy is to use the label content. + + // We construct a locator that finds a vaadin-side-nav-item that has this text. + // Since text is inside the item's shadow or light dom slots. + + return new SideNavigationItemElement( + getLocator().locator(SideNavigationItemElement.FIELD_TAG_NAME) + .filter(new Locator.FilterOptions().setHasText(label)).first()); + } + + /** + * Clicks an item by its label. + * + * @param label The label of the item to click. + */ + public void clickItem(String label) { + getItem(label).click(); + } + + /** + * Get the {@code SideNavigationElement} by its label. + * + * @param page the Playwright page + * @param label the accessible label of the side navigation + * @return the matching {@code SideNavigationElement} + */ + public static SideNavigationElement getByLabel(Page page, String label) { + return new SideNavigationElement( + page.getByRole(AriaRole.NAVIGATION, + new Page.GetByRoleOptions().setName(label) + ).and(page.locator(FIELD_TAG_NAME)).first()); + } + + /** + * Toggles the expansion state of the item. + */ + public void toggle() { + getLocator().locator("[slot='label']").first().click(); + } +} diff --git a/src/main/java/org/vaadin/addons/dramafinder/element/SideNavigationItemElement.java b/src/main/java/org/vaadin/addons/dramafinder/element/SideNavigationItemElement.java new file mode 100644 index 0000000..a6ab643 --- /dev/null +++ b/src/main/java/org/vaadin/addons/dramafinder/element/SideNavigationItemElement.java @@ -0,0 +1,92 @@ +package org.vaadin.addons.dramafinder.element; + +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.options.AriaRole; +import org.vaadin.addons.dramafinder.element.shared.HasEnabledElement; +import org.vaadin.addons.dramafinder.element.shared.HasLabelElement; +import org.vaadin.addons.dramafinder.element.shared.HasPrefixElement; +import org.vaadin.addons.dramafinder.element.shared.HasSuffixElement; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; + +/** + * PlaywrightElement for {@code }. + */ +@PlaywrightElement(SideNavigationItemElement.FIELD_TAG_NAME) +public class SideNavigationItemElement extends VaadinElement implements HasEnabledElement, HasPrefixElement, HasSuffixElement, HasLabelElement { + + public static final String FIELD_TAG_NAME = "vaadin-side-nav-item"; + + public SideNavigationItemElement(Locator locator) { + super(locator); + } + + /** + * Checks if the item is expanded. + */ + public boolean isExpanded() { + return getLocator().getAttribute("expanded") != null; + } + + /** + * Asserts that the item is expanded. + */ + public void assertExpanded() { + assertThat(getLocator()).hasAttribute("expanded", ""); + } + + /** + * Asserts that the item is collapsed. + */ + public void assertCollapsed() { + assertThat(getLocator()).not().hasAttribute("expanded", ""); + } + + /** + * Asserts that the item is enabled. + */ + @Override + public void assertEnabled() { + assertThat(getLocator()).not().hasAttribute("disabled", ""); + } + + /** + * Asserts that the item is disabled. + */ + @Override + public void assertDisabled() { + assertThat(getLocator()).hasAttribute("disabled", ""); + } + + /** + * Asserts that the item is current. + */ + public void assertCurrent() { + assertThat(getLocator()).hasAttribute("current", ""); + } + + /** + * Asserts that the item is not current. + */ + public void assertNotCurrent() { + assertThat(getLocator()).not().hasAttribute("current", ""); + } + + + /** + * Toggles the expansion state of the item. + * This relies on the toggle button inside the item. + */ + public void toggle() { + getLocator().locator("button[part='toggle-button']").first().click(); + } + + @Override + public Locator getLabelLocator() { + return getLocator(); + } + + public void navigate() { + getLocator().getByRole(AriaRole.LINK).first().click(); + } +} diff --git a/src/main/java/org/vaadin/addons/dramafinder/element/shared/HasLabelElement.java b/src/main/java/org/vaadin/addons/dramafinder/element/shared/HasLabelElement.java index 8077fc9..c670cbd 100644 --- a/src/main/java/org/vaadin/addons/dramafinder/element/shared/HasLabelElement.java +++ b/src/main/java/org/vaadin/addons/dramafinder/element/shared/HasLabelElement.java @@ -1,6 +1,7 @@ package org.vaadin.addons.dramafinder.element.shared; import com.microsoft.playwright.Locator; +import com.microsoft.playwright.assertions.LocatorAssertions; import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; @@ -9,20 +10,26 @@ */ public interface HasLabelElement extends HasLocatorElement { - /** Locator for the visible label element. */ + /** + * Locator for the visible label element. + */ default Locator getLabelLocator() { return getLocator().locator("label").first(); } - /** Get the label text. */ + /** + * Get the label text. + */ default String getLabel() { return getLabelLocator().textContent(); } - /** Assert that the label text matches, or is hidden when null. */ + /** + * Assert that the label text matches, or is hidden when null. + */ default void assertLabel(String label) { if (label != null) { - assertThat(getLabelLocator()).hasText(label); + assertThat(getLabelLocator()).hasText(label, new LocatorAssertions.HasTextOptions().setUseInnerText(true)); } else { assertThat(getLabelLocator()).isHidden(); } diff --git a/src/test/java/org/vaadin/addons/dramafinder/tests/it/SideNavigationElementViewIT.java b/src/test/java/org/vaadin/addons/dramafinder/tests/it/SideNavigationElementViewIT.java new file mode 100644 index 0000000..ca9643a --- /dev/null +++ b/src/test/java/org/vaadin/addons/dramafinder/tests/it/SideNavigationElementViewIT.java @@ -0,0 +1,141 @@ +package org.vaadin.addons.dramafinder.tests.it; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.vaadin.addons.dramafinder.HasTestView; +import org.vaadin.addons.dramafinder.element.SideNavigationElement; +import org.vaadin.addons.dramafinder.element.SideNavigationItemElement; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class SideNavigationElementViewIT extends SpringPlaywrightIT implements HasTestView { + + @Override + public String getView() { + return "side-navigation-element-view"; + } + + @Test + public void testSideNavCollapsible() { + SideNavigationElement nav = SideNavigationElement.getByLabel(page, "My App"); + nav.assertVisible(); + nav.assertCollapsible(); + nav.assertExpanded(); + nav.toggle(); + nav.assertCollapsed(); + nav.toggle(); + nav.assertExpanded(); + } + + @Test + public void testSideNavStructure() { + SideNavigationElement nav = SideNavigationElement.getByLabel(page, "My App"); + nav.assertVisible(); + nav.assertExpanded(); + + // Check Label (Header) + assertEquals("My App", nav.getLabel()); + + // Check flat item + SideNavigationItemElement dashboard = nav.getItem("Dashboard"); + dashboard.assertVisible(); + dashboard.assertEnabled(); + dashboard.assertLabel("Dashboard"); + assertEquals("Dashboard", dashboard.getLabel()); + + // Check nested item + SideNavigationItemElement admin = nav.getItem("Admin"); + admin.assertVisible(); + + // It has children, so it should be expandable. Validating default state. + // Usually dependent on whether it's expanded by default. + // Vaadin SideNavItem with children is collapsible. + // Let's check if we can find children. + + SideNavigationItemElement users = nav.getItem("Users"); + // Depending on whether Admin is expanded, Users might be visible or not? + // Actually locally in valid SideNav, items are in light dom or shadow? + // They are usually always in DOM but hidden via CSS if collapsed? + // assertVisible() checks visibility. + // Assuming default is expanded or we toggle it. + + if (!admin.isExpanded()) { + admin.toggle(); + } + admin.assertExpanded(); + users.assertVisible(); + } + + @Test + public void testItemState() { + SideNavigationElement nav = SideNavigationElement.getByLabel(page, "My App"); + SideNavigationItemElement reports = nav.getItem("Reports"); + reports.assertVisible(); + reports.assertDisabled(); + } + + @Test + public void testItemCurrentState() { + SideNavigationElement nav = SideNavigationElement.getByLabel(page, "My App"); + SideNavigationItemElement messages = nav.getItem("Messages"); + messages.assertCurrent(); + SideNavigationItemElement reports = nav.getItem("Reports"); + reports.assertNotCurrent(); + } + + @Test + public void testToggle() { + SideNavigationElement nav = SideNavigationElement.getByLabel(page, "My App"); + SideNavigationItemElement admin = nav.getItem("Admin"); + + admin.assertCollapsed(); + // Ensure state matches expectation + boolean initialExpanded = admin.isExpanded(); + + admin.toggle(); + if (initialExpanded) { + admin.assertCollapsed(); + } else { + admin.assertExpanded(); + } + + admin.toggle(); + if (initialExpanded) { + admin.assertExpanded(); + } else { + admin.assertCollapsed(); + } + } + + @Test + public void testToggleOnClick() { + SideNavigationElement nav = SideNavigationElement.getByLabel(page, "My App"); + SideNavigationItemElement admin = nav.getItem("Admin"); + + admin.assertCollapsed(); + admin.click(); + admin.assertExpanded(); + + } + + @Test + public void testNavigate() { + SideNavigationElement nav = SideNavigationElement.getByLabel(page, "My App"); + SideNavigationItemElement reports = nav.getItem("Dashboard"); + assertThat(page).hasURL(getUrl() + getView()); + reports.navigate(); + assertThat(page).not().hasURL(getUrl()); + } + + @Test + public void testNavigateOnClick() { + SideNavigationElement nav = SideNavigationElement.getByLabel(page, "My App"); + SideNavigationItemElement reports = nav.getItem("Dashboard"); + assertThat(page).hasURL(getUrl() + getView()); + reports.click(); + assertThat(page).not().hasURL(getUrl()); + } +} diff --git a/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/SideNavigationElementView.java b/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/SideNavigationElementView.java new file mode 100644 index 0000000..2df56a3 --- /dev/null +++ b/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/SideNavigationElementView.java @@ -0,0 +1,52 @@ +package org.vaadin.addons.dramafinder.tests.testuis; + +import com.vaadin.flow.component.html.Main; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@PageTitle("Side Navigation Demo") +@Route(value = "side-navigation-element-view", layout = MainLayout.class) +public class SideNavigationElementView extends Main { + + public SideNavigationElementView() { + SideNav nav = new SideNav("My App"); + nav.setCollapsible(true); + nav.setId("default-nav"); + + SideNavItem dashboard = new SideNavItem("Dashboard"); + dashboard.setPath(""); + nav.addItem(dashboard); + + SideNavItem messages = new SideNavItem("Messages"); + messages.setPath("side-navigation-element-view"); + nav.addItem(messages); + + SideNavItem admin = new SideNavItem("Admin"); + admin.setPrefixComponent(VaadinIcon.COG.create()); + admin.setSuffixComponent(VaadinIcon.ACCORDION_MENU.create()); + + SideNavItem users = new SideNavItem("Users"); + users.setPath("admin/users"); + + SideNavItem roles = new SideNavItem("Roles"); + roles.setPath("admin/roles"); + + admin.addItem(users, roles); + nav.addItem(admin); + + // Disabled item + SideNavItem reports = new SideNavItem("Reports"); + reports.setEnabled(false); + nav.addItem(reports); + + add(nav); + + Span activeItemSpan = new Span(); + activeItemSpan.setId("active-item"); + add(activeItemSpan); + } +}