diff --git a/flow-components-bom/pom.xml b/flow-components-bom/pom.xml index dd7f76b1040..b9f48404fac 100644 --- a/flow-components-bom/pom.xml +++ b/flow-components-bom/pom.xml @@ -52,6 +52,16 @@ vaadin-board-testbench ${project.version} + + com.vaadin + vaadin-breadcrumb-flow + ${project.version} + + + com.vaadin + vaadin-breadcrumb-testbench + ${project.version} + com.vaadin vaadin-button-flow diff --git a/pom.xml b/pom.xml index 743624eb407..c6a03974a33 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ vaadin-accordion-flow-parent vaadin-avatar-flow-parent vaadin-app-layout-flow-parent + vaadin-breadcrumb-flow-parent vaadin-button-flow-parent vaadin-card-flow-parent vaadin-checkbox-flow-parent diff --git a/vaadin-breadcrumb-flow-parent/pom.xml b/vaadin-breadcrumb-flow-parent/pom.xml new file mode 100644 index 00000000000..8f65a4d05c9 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + com.vaadin + vaadin-flow-components + 25.0-SNAPSHOT + + vaadin-breadcrumb-flow-parent + pom + Vaadin Breadcrumb Parent + Vaadin Breadcrumb Parent + + vaadin-breadcrumb-flow + vaadin-breadcrumb-testbench + + + + + default + + + !release + + + + vaadin-breadcrumb-flow-integration-tests + + + + diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow-integration-tests/pom.xml b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow-integration-tests/pom.xml new file mode 100644 index 00000000000..c85a1a99262 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow-integration-tests/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + com.vaadin + vaadin-breadcrumb-flow-parent + 25.0-SNAPSHOT + + vaadin-breadcrumb-integration-tests + war + Vaadin Breadcrumb Integration Tests + Vaadin Breadcrumb Integration Tests + + + com.vaadin + flow-client + ${flow.version} + + + com.vaadin + flow-html-components + ${flow.version} + + + com.vaadin + flow-lit-template + + + com.vaadin + flow-polymer-template + + + com.vaadin + flow-test-generic + test + + + com.vaadin + flow-test-util + test + + + com.vaadin + vaadin-breadcrumb-flow + ${project.version} + + + com.vaadin + vaadin-breadcrumb-testbench + ${project.version} + + + com.vaadin + vaadin-dev-server + + + com.vaadin + vaadin-flow-components-test-util + ${project.version} + test + + + com.vaadin + vaadin-lumo-theme + ${project.version} + + + com.vaadin + vaadin-ordered-layout-flow + ${project.version} + + + com.vaadin + vaadin-testbench-core + test + + + org.slf4j + slf4j-simple + 2.0.17 + + + + + + maven-clean-plugin + + + + ${project.basedir} + + package*.json + pnpm* + vite.generated.ts + types.d.ts + tsconfig.json + frontend/routes.tsx + frontend/App.tsx + + + + ${project.basedir}/node_modules + + + ${project.basedir}/frontend + + generated/vaadin.ts + generated/vite-devmode.ts + generated/jar-resources/ + generated/theme/ + + + + + + + com.vaadin + flow-maven-plugin + ${flow.version} + + + + prepare-frontend + build-frontend + + + + + + org.eclipse.jetty + jetty-maven-plugin + ${jetty.version} + + 0 + + 5 + + + + + + diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/pom.xml b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/pom.xml new file mode 100644 index 00000000000..fdd9eb26447 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + com.vaadin + vaadin-breadcrumb-flow-parent + 25.0-SNAPSHOT + + vaadin-breadcrumb-flow + jar + Vaadin Breadcrumb + Vaadin Breadcrumb + + + com.vaadin + flow-server + provided + + + com.vaadin + flow-test-generic + test + + + com.vaadin + flow-test-util + test + + + com.vaadin + vaadin-flow-components-base + ${project.version} + + + jakarta.platform + jakarta.jakartaee-web-api + test + + + jakarta.servlet + jakarta.servlet-api + + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + + + + biz.aQute.bnd + bnd-maven-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + + + attach-docs + + + with-docs + + + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.codehaus.mojo + build-helper-maven-plugin + + + + + + diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/main/java/com/vaadin/flow/component/breadcrumb/Breadcrumb.java b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/main/java/com/vaadin/flow/component/breadcrumb/Breadcrumb.java new file mode 100644 index 00000000000..d34aa602acc --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/main/java/com/vaadin/flow/component/breadcrumb/Breadcrumb.java @@ -0,0 +1,202 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.breadcrumb; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasSize; +import com.vaadin.flow.component.HasStyle; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.dependency.NpmPackage; +import com.vaadin.flow.component.shared.HasThemeVariant; +import com.vaadin.flow.dom.Element; + +/** + * Breadcrumb is a navigation component that shows a hierarchical path through + * the application's structure, allowing users to navigate back to previous + * levels. + *

+ * Use Breadcrumb when you want to show the user's current location within a + * hierarchy and provide navigation to parent levels. + *

+ * {@link BreadcrumbItem} components can be added to this component with the + * {@link #add(BreadcrumbItem...)} method or the + * {@link #Breadcrumb(BreadcrumbItem...)} constructor. + * + * @author Vaadin Ltd. + */ +@Tag("vaadin-breadcrumb") +@JsModule("@vaadin/breadcrumb/src/vaadin-breadcrumb.js") +@NpmPackage(value = "@vaadin/breadcrumb", version = "25.0.0-alpha16") +public class Breadcrumb extends Component + implements HasSize, HasStyle, HasThemeVariant { + + /** + * Constructs an empty breadcrumb component. + */ + public Breadcrumb() { + super(); + } + + /** + * Constructs a breadcrumb component with the given items. + * + * @param items + * the breadcrumb items to add + */ + public Breadcrumb(BreadcrumbItem... items) { + this(); + add(items); + } + + /** + * Adds the given breadcrumb items to the component. + * + * @param items + * the items to add, not {@code null} + */ + public void add(BreadcrumbItem... items) { + Objects.requireNonNull(items, "Items to add cannot be null"); + Arrays.stream(items) + .map(item -> Objects.requireNonNull(item, + "Individual item to add cannot be null")) + .map(BreadcrumbItem::getElement) + .forEach(getElement()::appendChild); + } + + /** + * Removes the given breadcrumb items from the component. + * + * @param items + * the items to remove, not {@code null} + */ + public void remove(BreadcrumbItem... items) { + Objects.requireNonNull(items, "Items to remove cannot be null"); + Arrays.stream(items) + .map(item -> Objects.requireNonNull(item, + "Individual item to remove cannot be null")) + .map(BreadcrumbItem::getElement) + .forEach(getElement()::removeChild); + } + + /** + * Removes all breadcrumb items from the component. + */ + public void removeAll() { + getElement().removeAllChildren(); + } + + /** + * Gets the breadcrumb item at the given index. + * + * @param index + * the index of the item to get + * @return the item at the given index + * @throws IndexOutOfBoundsException + * if the index is out of range + */ + public BreadcrumbItem getItemAt(int index) { + if (index < 0 || index >= getItemCount()) { + throw new IndexOutOfBoundsException( + "Index: " + index + ", Size: " + getItemCount()); + } + Iterator iterator = getItems().iterator(); + for (int i = 0; i < index; i++) { + iterator.next(); + } + return iterator.next(); + } + + /** + * Gets the number of breadcrumb items in the component. + * + * @return the number of items + */ + public int getItemCount() { + return (int) getItems().count(); + } + + /** + * Gets the index of the given breadcrumb item. + * + * @param item + * the item to get the index of + * @return the index of the item, or -1 if not found + */ + public int indexOf(BreadcrumbItem item) { + if (item == null) { + return -1; + } + Iterator iterator = getItems().iterator(); + int index = 0; + while (iterator.hasNext()) { + if (iterator.next().equals(item)) { + return index; + } + index++; + } + return -1; + } + + /** + * Replaces the breadcrumb item at the given index with a new item. + * + * @param index + * the index of the item to replace + * @param newItem + * the new item to set, not {@code null} + * @throws IndexOutOfBoundsException + * if the index is out of range + */ + public void replace(int index, BreadcrumbItem newItem) { + Objects.requireNonNull(newItem, "New item cannot be null"); + if (index < 0 || index >= getItemCount()) { + throw new IndexOutOfBoundsException( + "Index: " + index + ", Size: " + getItemCount()); + } + + BreadcrumbItem oldItem = getItemAt(index); + Element parentElement = getElement(); + List children = new ArrayList<>(); + parentElement.getChildren().forEach(children::add); + + int elementIndex = children.indexOf(oldItem.getElement()); + if (elementIndex >= 0) { + parentElement.insertChild(elementIndex, newItem.getElement()); + parentElement.removeChild(oldItem.getElement()); + } + } + + /** + * Gets all breadcrumb items in the component as a stream. + * + * @return a stream of all items + */ + public Stream getItems() { + return getElement().getChildren() + .filter(element -> element.getComponent() + .filter(BreadcrumbItem.class::isInstance).isPresent()) + .map(element -> element.getComponent() + .map(BreadcrumbItem.class::cast).get()); + } +} diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/main/java/com/vaadin/flow/component/breadcrumb/BreadcrumbItem.java b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/main/java/com/vaadin/flow/component/breadcrumb/BreadcrumbItem.java new file mode 100644 index 00000000000..6693c7f5588 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/main/java/com/vaadin/flow/component/breadcrumb/BreadcrumbItem.java @@ -0,0 +1,319 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.breadcrumb; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.HasEnabled; +import com.vaadin.flow.component.HasStyle; +import com.vaadin.flow.component.HasText; +import com.vaadin.flow.component.PropertyDescriptor; +import com.vaadin.flow.component.PropertyDescriptors; +import com.vaadin.flow.component.Synchronize; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.dependency.NpmPackage; +import com.vaadin.flow.component.shared.HasTooltip; +import com.vaadin.flow.router.QueryParameters; +import com.vaadin.flow.router.RouteConfiguration; +import com.vaadin.flow.router.RouteParameters; +import com.vaadin.flow.router.Router; +import com.vaadin.flow.server.VaadinService; + +/** + * A single item in a {@link Breadcrumb} component. + *

+ * BreadcrumbItem can contain text and/or components. It supports navigation + * through href property or by using Router navigation targets. + * + * @author Vaadin Ltd. + */ +@Tag("vaadin-breadcrumb-item") +@JsModule("@vaadin/breadcrumb/src/vaadin-breadcrumb-item.js") +@NpmPackage(value = "@vaadin/breadcrumb", version = "25.0.0-alpha16") +public class BreadcrumbItem extends Component + implements HasComponents, HasText, HasStyle, HasEnabled, HasTooltip { + + private static final PropertyDescriptor hrefDescriptor = PropertyDescriptors + .propertyWithDefault("href", ""); + + private static final PropertyDescriptor targetDescriptor = PropertyDescriptors + .propertyWithDefault("target", ""); + + private static final PropertyDescriptor routerIgnoreDescriptor = PropertyDescriptors + .propertyWithDefault("routerIgnore", false); + + /** + * Constructs an empty breadcrumb item. + */ + public BreadcrumbItem() { + super(); + } + + /** + * Constructs a breadcrumb item with the given text. + * + * @param text + * the text content + */ + public BreadcrumbItem(String text) { + this(); + setText(text); + } + + /** + * Constructs a breadcrumb item with the given text and href. + * + * @param text + * the text content + * @param href + * the href to navigate to + */ + public BreadcrumbItem(String text, String href) { + this(text); + setHref(href); + } + + /** + * Constructs a breadcrumb item with the given component. + * + * @param component + * the component to add + */ + public BreadcrumbItem(Component component) { + this(); + add(component); + } + + /** + * Constructs a breadcrumb item with the given component and href. + * + * @param component + * the component to add + * @param href + * the href to navigate to + */ + public BreadcrumbItem(Component component, String href) { + this(component); + setHref(href); + } + + /** + * Constructs a breadcrumb item with the given text and navigation target. + * + * @param text + * the text content + * @param navigationTarget + * the navigation target class + */ + public BreadcrumbItem(String text, + Class navigationTarget) { + this(text); + setRoute(navigationTarget); + } + + /** + * Constructs a breadcrumb item with the given text, navigation target, and + * route parameters. + * + * @param text + * the text content + * @param navigationTarget + * the navigation target class + * @param routeParameters + * the route parameters + */ + public BreadcrumbItem(String text, + Class navigationTarget, + RouteParameters routeParameters) { + this(text); + setRoute(navigationTarget, routeParameters); + } + + /** + * Gets the href of this item. + * + * @return the href, or an empty string if no href is set + */ + public String getHref() { + return get(hrefDescriptor); + } + + /** + * Sets the href of this item. + *

+ * The href is the URL that the item links to. Set to an empty string to + * remove the href. + * + * @param href + * the href to set, or an empty string to remove + */ + public void setHref(String href) { + set(hrefDescriptor, href == null ? "" : href); + } + + /** + * Gets the target of this item's link. + * + * @return the target, or an empty string if no target is set + */ + public String getTarget() { + return get(targetDescriptor); + } + + /** + * Sets the target of this item's link. + *

+ * The target attribute specifies where to display the linked URL. Common + * values are "_blank" to open in a new tab, "_self" to open in the same + * frame, "_parent" to open in the parent frame, or "_top" to open in the + * full window. + * + * @param target + * the target to set, or an empty string to remove + */ + public void setTarget(String target) { + set(targetDescriptor, target == null ? "" : target); + } + + /** + * Gets whether this item should be ignored by client-side routers. + * + * @return {@code true} if router should ignore this item, {@code false} + * otherwise + */ + public boolean isRouterIgnore() { + return get(routerIgnoreDescriptor); + } + + /** + * Sets whether this item should be ignored by client-side routers. + *

+ * When set to {@code true}, clicking this item will cause a full page + * reload instead of client-side navigation. + * + * @param routerIgnore + * {@code true} to ignore client-side routing, {@code false} + * otherwise + */ + public void setRouterIgnore(boolean routerIgnore) { + set(routerIgnoreDescriptor, routerIgnore); + } + + /** + * Gets whether this item represents the current page. + *

+ * This property is automatically updated based on the current URL. + * + * @return {@code true} if this item represents the current page, + * {@code false} otherwise + */ + @Synchronize(property = "current", value = "current-changed") + public boolean isCurrent() { + return getElement().getProperty("current", false); + } + + /** + * Sets the navigation target for this item using a router class. + * + * @param navigationTarget + * the navigation target class + */ + public void setRoute(Class navigationTarget) { + setRoute(navigationTarget, RouteParameters.empty()); + } + + /** + * Sets the navigation target for this item using a router class and route + * parameters. + * + * @param navigationTarget + * the navigation target class + * @param routeParameters + * the route parameters + */ + public void setRoute(Class navigationTarget, + RouteParameters routeParameters) { + setRoute(getRouter(), navigationTarget, routeParameters, + QueryParameters.empty()); + } + + /** + * Sets the navigation target for this item using a router class, route + * parameters, and query parameters. + * + * @param navigationTarget + * the navigation target class + * @param routeParameters + * the route parameters + * @param queryParameters + * the query parameters + */ + public void setRoute(Class navigationTarget, + RouteParameters routeParameters, QueryParameters queryParameters) { + setRoute(getRouter(), navigationTarget, routeParameters, + queryParameters); + } + + /** + * Sets the navigation target for this item using a specific router. + * + * @param router + * the router to use, or {@code null} to use the default + * @param navigationTarget + * the navigation target class + * @param routeParameters + * the route parameters + * @param queryParameters + * the query parameters + */ + public void setRoute(Router router, + Class navigationTarget, + RouteParameters routeParameters, QueryParameters queryParameters) { + if (router == null) { + router = getRouter(); + } + + if (navigationTarget != null) { + String url = RouteConfiguration.forRegistry(router.getRegistry()) + .getUrl(navigationTarget, routeParameters); + + if (!queryParameters.getParameters().isEmpty()) { + url = url + "?" + queryParameters.getQueryString(); + } + + setHref(url); + } else { + setHref(""); + } + } + + private Router getRouter() { + Router router = null; + if (getElement().getNode().isAttached()) { + router = getUI().map(ui -> ui.getInternals().getRouter()) + .orElse(null); + } + if (router == null) { + router = VaadinService.getCurrent().getRouter(); + } + if (router == null) { + throw new IllegalStateException( + "Cannot find a router to use for navigation"); + } + return router; + } +} diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/main/java/com/vaadin/flow/component/breadcrumb/BreadcrumbVariant.java b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/main/java/com/vaadin/flow/component/breadcrumb/BreadcrumbVariant.java new file mode 100644 index 00000000000..f480edb72f1 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/main/java/com/vaadin/flow/component/breadcrumb/BreadcrumbVariant.java @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.breadcrumb; + +import com.vaadin.flow.component.shared.ThemeVariant; + +/** + * Theme variants for the {@link Breadcrumb} component. + * + * @author Vaadin Ltd. + */ +public enum BreadcrumbVariant implements ThemeVariant { + + /** + * Small size variant. + */ + LUMO_SMALL("small"), + + /** + * Large size variant. + */ + LUMO_LARGE("large"); + + private final String variant; + + BreadcrumbVariant(String variant) { + this.variant = variant; + } + + /** + * Gets the variant name. + * + * @return variant name + */ + @Override + public String getVariantName() { + return variant; + } +} diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/test/java/com/vaadin/flow/component/breadcrumb/BreadcrumbItemTest.java b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/test/java/com/vaadin/flow/component/breadcrumb/BreadcrumbItemTest.java new file mode 100644 index 00000000000..74c89e0e015 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/test/java/com/vaadin/flow/component/breadcrumb/BreadcrumbItemTest.java @@ -0,0 +1,242 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.breadcrumb; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.RouteParameters; + +/** + * Tests for {@link BreadcrumbItem}. + */ +public class BreadcrumbItemTest { + + @Test + public void createEmptyItem() { + BreadcrumbItem item = new BreadcrumbItem(); + + Assert.assertEquals("Tag name is invalid", "vaadin-breadcrumb-item", + item.getElement().getTag()); + Assert.assertEquals("Text should be empty", "", item.getText()); + Assert.assertEquals("Href should be empty", "", item.getHref()); + } + + @Test + public void createItemWithText() { + BreadcrumbItem item = new BreadcrumbItem("Home"); + + Assert.assertEquals("Text is invalid", "Home", item.getText()); + Assert.assertEquals("Href should be empty", "", item.getHref()); + } + + @Test + public void createItemWithTextAndHref() { + BreadcrumbItem item = new BreadcrumbItem("Home", "/home"); + + Assert.assertEquals("Text is invalid", "Home", item.getText()); + Assert.assertEquals("Href is invalid", "/home", item.getHref()); + } + + @Test + public void createItemWithComponent() { + Div div = new Div("Content"); + BreadcrumbItem item = new BreadcrumbItem(div); + + Assert.assertEquals("Child count is invalid", 1, + item.getElement().getChildCount()); + Assert.assertEquals("Child component is invalid", div.getElement(), + item.getElement().getChild(0)); + } + + @Test + public void createItemWithComponentAndHref() { + Div div = new Div("Content"); + BreadcrumbItem item = new BreadcrumbItem(div, "/home"); + + Assert.assertEquals("Child count is invalid", 1, + item.getElement().getChildCount()); + Assert.assertEquals("Href is invalid", "/home", item.getHref()); + } + + @Test + public void setHref() { + BreadcrumbItem item = new BreadcrumbItem(); + + item.setHref("/products"); + Assert.assertEquals("Href is invalid", "/products", item.getHref()); + + item.setHref(null); + Assert.assertEquals("Href should be empty", "", item.getHref()); + + item.setHref(""); + Assert.assertEquals("Href should be empty", "", item.getHref()); + } + + @Test + public void setTarget() { + BreadcrumbItem item = new BreadcrumbItem(); + + item.setTarget("_blank"); + Assert.assertEquals("Target is invalid", "_blank", item.getTarget()); + + item.setTarget(null); + Assert.assertEquals("Target should be empty", "", item.getTarget()); + + item.setTarget(""); + Assert.assertEquals("Target should be empty", "", item.getTarget()); + } + + @Test + public void setRouterIgnore() { + BreadcrumbItem item = new BreadcrumbItem(); + + Assert.assertFalse("Default routerIgnore should be false", + item.isRouterIgnore()); + + item.setRouterIgnore(true); + Assert.assertTrue("RouterIgnore should be true", item.isRouterIgnore()); + + item.setRouterIgnore(false); + Assert.assertFalse("RouterIgnore should be false", + item.isRouterIgnore()); + } + + @Test + public void setText() { + BreadcrumbItem item = new BreadcrumbItem(); + + item.setText("Products"); + Assert.assertEquals("Text is invalid", "Products", item.getText()); + + item.setText(null); + Assert.assertEquals("Text should be empty", "", item.getText()); + } + + @Test + public void addComponent() { + BreadcrumbItem item = new BreadcrumbItem(); + Div div1 = new Div("First"); + Div div2 = new Div("Second"); + + item.add(div1, div2); + + Assert.assertEquals("Child count is invalid", 2, + item.getElement().getChildCount()); + Assert.assertEquals("First child is invalid", div1.getElement(), + item.getElement().getChild(0)); + Assert.assertEquals("Second child is invalid", div2.getElement(), + item.getElement().getChild(1)); + } + + @Test + public void removeComponent() { + BreadcrumbItem item = new BreadcrumbItem(); + Div div1 = new Div("First"); + Div div2 = new Div("Second"); + + item.add(div1, div2); + item.remove(div1); + + Assert.assertEquals("Child count is invalid", 1, + item.getElement().getChildCount()); + Assert.assertEquals("Remaining child is invalid", div2.getElement(), + item.getElement().getChild(0)); + } + + @Test + public void removeAllComponents() { + BreadcrumbItem item = new BreadcrumbItem(); + Div div1 = new Div("First"); + Div div2 = new Div("Second"); + + item.add(div1, div2); + item.removeAll(); + + Assert.assertEquals("Child count should be 0", 0, + item.getElement().getChildCount()); + } + + @Test + public void setEnabled() { + BreadcrumbItem item = new BreadcrumbItem(); + + Assert.assertTrue("Default enabled should be true", item.isEnabled()); + + item.setEnabled(false); + Assert.assertFalse("Enabled should be false", item.isEnabled()); + + item.setEnabled(true); + Assert.assertTrue("Enabled should be true", item.isEnabled()); + } + + @Test + public void setTooltip() { + BreadcrumbItem item = new BreadcrumbItem(); + + // HasTooltip creates a tooltip immediately, so we check if text is + // null/empty + Assert.assertTrue("Default tooltip text should be null or empty", + item.getTooltip() == null || item.getTooltip().getText() == null + || item.getTooltip().getText().isEmpty()); + + item.setTooltipText("This is a tooltip"); + Assert.assertNotNull("Tooltip should not be null", item.getTooltip()); + Assert.assertEquals("Tooltip text is invalid", "This is a tooltip", + item.getTooltip().getText()); + } + + @Test + public void constructorsWithNavigationTarget() { + // Test constructor with navigation target - will throw without router + try { + BreadcrumbItem item1 = new BreadcrumbItem("Test", TestView.class); + Assert.fail("Should throw exception without router"); + } catch (IllegalStateException | NullPointerException e) { + // Expected if router configuration is not available + // Can be either IllegalStateException or NullPointerException + // depending on the state + Assert.assertTrue("Expected router exception", + e.getMessage() != null + && (e.getMessage().contains("Cannot find a router") + || e.getMessage().contains( + "VaadinService.getCurrent()"))); + } + + // Test constructor with navigation target and route parameters - will + // throw without router + try { + RouteParameters params = RouteParameters.empty(); + BreadcrumbItem item2 = new BreadcrumbItem("Test", TestView.class, + params); + Assert.fail("Should throw exception without router"); + } catch (IllegalStateException | NullPointerException e) { + // Expected if router configuration is not available + // Can be either IllegalStateException or NullPointerException + // depending on the state + Assert.assertTrue("Expected router exception", + e.getMessage() != null + && (e.getMessage().contains("Cannot find a router") + || e.getMessage().contains( + "VaadinService.getCurrent()"))); + } + } + + // Test view class for navigation tests + private static class TestView extends Div { + } +} diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/test/java/com/vaadin/flow/component/breadcrumb/BreadcrumbTest.java b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/test/java/com/vaadin/flow/component/breadcrumb/BreadcrumbTest.java new file mode 100644 index 00000000000..09b913f92b1 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-flow/src/test/java/com/vaadin/flow/component/breadcrumb/BreadcrumbTest.java @@ -0,0 +1,250 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.breadcrumb; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * Tests for {@link Breadcrumb}. + */ +public class BreadcrumbTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createBreadcrumbInDefaultState() { + Breadcrumb breadcrumb = new Breadcrumb(); + + Assert.assertEquals("Initial item count is invalid", 0, + breadcrumb.getItemCount()); + Assert.assertEquals("Tag name is invalid", "vaadin-breadcrumb", + breadcrumb.getElement().getTag()); + } + + @Test + public void createBreadcrumbWithItems() { + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + BreadcrumbItem item2 = new BreadcrumbItem("Products"); + BreadcrumbItem item3 = new BreadcrumbItem("Details"); + Breadcrumb breadcrumb = new Breadcrumb(item1, item2, item3); + + Assert.assertEquals("Initial item count is invalid", 3, + breadcrumb.getItemCount()); + Assert.assertEquals("First item is invalid", item1, + breadcrumb.getItemAt(0)); + Assert.assertEquals("Second item is invalid", item2, + breadcrumb.getItemAt(1)); + Assert.assertEquals("Third item is invalid", item3, + breadcrumb.getItemAt(2)); + } + + @Test + public void addItems() { + Breadcrumb breadcrumb = new Breadcrumb(); + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + BreadcrumbItem item2 = new BreadcrumbItem("Products"); + + breadcrumb.add(item1, item2); + + Assert.assertEquals("Item count after add is invalid", 2, + breadcrumb.getItemCount()); + Assert.assertEquals("First item is invalid", item1, + breadcrumb.getItemAt(0)); + Assert.assertEquals("Second item is invalid", item2, + breadcrumb.getItemAt(1)); + } + + @Test + public void addNullItems_throwsException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("Items to add cannot be null"); + + Breadcrumb breadcrumb = new Breadcrumb(); + breadcrumb.add((BreadcrumbItem[]) null); + } + + @Test + public void addItemWithNull_throwsException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("Individual item to add cannot be null"); + + Breadcrumb breadcrumb = new Breadcrumb(); + breadcrumb.add(new BreadcrumbItem("Home"), null); + } + + @Test + public void removeItems() { + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + BreadcrumbItem item2 = new BreadcrumbItem("Products"); + BreadcrumbItem item3 = new BreadcrumbItem("Details"); + Breadcrumb breadcrumb = new Breadcrumb(item1, item2, item3); + + breadcrumb.remove(item2); + + Assert.assertEquals("Item count after remove is invalid", 2, + breadcrumb.getItemCount()); + Assert.assertEquals("First item is invalid", item1, + breadcrumb.getItemAt(0)); + Assert.assertEquals("Second item is invalid", item3, + breadcrumb.getItemAt(1)); + } + + @Test + public void removeAll() { + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + BreadcrumbItem item2 = new BreadcrumbItem("Products"); + Breadcrumb breadcrumb = new Breadcrumb(item1, item2); + + breadcrumb.removeAll(); + + Assert.assertEquals("Item count after removeAll is invalid", 0, + breadcrumb.getItemCount()); + } + + @Test + public void getItemAt_invalidIndex_throwsException() { + thrown.expect(IndexOutOfBoundsException.class); + thrown.expectMessage("Index: 0, Size: 0"); + + Breadcrumb breadcrumb = new Breadcrumb(); + breadcrumb.getItemAt(0); + } + + @Test + public void getItemAt_negativeIndex_throwsException() { + thrown.expect(IndexOutOfBoundsException.class); + thrown.expectMessage("Index: -1, Size: 0"); + + Breadcrumb breadcrumb = new Breadcrumb(); + breadcrumb.getItemAt(-1); + } + + @Test + public void indexOf_returnsCorrectIndex() { + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + BreadcrumbItem item2 = new BreadcrumbItem("Products"); + BreadcrumbItem item3 = new BreadcrumbItem("Details"); + Breadcrumb breadcrumb = new Breadcrumb(item1, item2, item3); + + Assert.assertEquals("Index of item1 is invalid", 0, + breadcrumb.indexOf(item1)); + Assert.assertEquals("Index of item2 is invalid", 1, + breadcrumb.indexOf(item2)); + Assert.assertEquals("Index of item3 is invalid", 2, + breadcrumb.indexOf(item3)); + } + + @Test + public void indexOf_itemNotFound_returnsNegativeOne() { + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + BreadcrumbItem item2 = new BreadcrumbItem("Products"); + BreadcrumbItem itemNotAdded = new BreadcrumbItem("Details"); + Breadcrumb breadcrumb = new Breadcrumb(item1, item2); + + Assert.assertEquals("Index of not added item should be -1", -1, + breadcrumb.indexOf(itemNotAdded)); + } + + @Test + public void indexOf_nullItem_returnsNegativeOne() { + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + Breadcrumb breadcrumb = new Breadcrumb(item1); + + Assert.assertEquals("Index of null should be -1", -1, + breadcrumb.indexOf(null)); + } + + @Test + public void replace_validIndex() { + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + BreadcrumbItem item2 = new BreadcrumbItem("Products"); + BreadcrumbItem item3 = new BreadcrumbItem("Details"); + BreadcrumbItem newItem = new BreadcrumbItem("Categories"); + Breadcrumb breadcrumb = new Breadcrumb(item1, item2, item3); + + breadcrumb.replace(1, newItem); + + Assert.assertEquals("Item count after replace is invalid", 3, + breadcrumb.getItemCount()); + Assert.assertEquals("First item is invalid", item1, + breadcrumb.getItemAt(0)); + Assert.assertEquals("Replaced item is invalid", newItem, + breadcrumb.getItemAt(1)); + Assert.assertEquals("Third item is invalid", item3, + breadcrumb.getItemAt(2)); + } + + @Test + public void replace_invalidIndex_throwsException() { + thrown.expect(IndexOutOfBoundsException.class); + thrown.expectMessage("Index: 3, Size: 2"); + + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + BreadcrumbItem item2 = new BreadcrumbItem("Products"); + BreadcrumbItem newItem = new BreadcrumbItem("Categories"); + Breadcrumb breadcrumb = new Breadcrumb(item1, item2); + + breadcrumb.replace(3, newItem); + } + + @Test + public void replace_nullItem_throwsException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("New item cannot be null"); + + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + Breadcrumb breadcrumb = new Breadcrumb(item1); + + breadcrumb.replace(0, null); + } + + @Test + public void getItems_returnsAllItems() { + BreadcrumbItem item1 = new BreadcrumbItem("Home"); + BreadcrumbItem item2 = new BreadcrumbItem("Products"); + BreadcrumbItem item3 = new BreadcrumbItem("Details"); + Breadcrumb breadcrumb = new Breadcrumb(item1, item2, item3); + + BreadcrumbItem[] items = breadcrumb.getItems() + .toArray(BreadcrumbItem[]::new); + + Assert.assertEquals("Item count is invalid", 3, items.length); + Assert.assertEquals("First item is invalid", item1, items[0]); + Assert.assertEquals("Second item is invalid", item2, items[1]); + Assert.assertEquals("Third item is invalid", item3, items[2]); + } + + @Test + public void themeVariants() { + Breadcrumb breadcrumb = new Breadcrumb(); + + breadcrumb.addThemeVariants(BreadcrumbVariant.LUMO_SMALL); + Assert.assertTrue("Should have small variant", + breadcrumb.getThemeNames().contains("small")); + + breadcrumb.addThemeVariants(BreadcrumbVariant.LUMO_LARGE); + Assert.assertTrue("Should have large variant", + breadcrumb.getThemeNames().contains("large")); + + breadcrumb.removeThemeVariants(BreadcrumbVariant.LUMO_SMALL); + Assert.assertFalse("Should not have small variant", + breadcrumb.getThemeNames().contains("small")); + } +} diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-testbench/pom.xml b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-testbench/pom.xml new file mode 100644 index 00000000000..dc35a31cd09 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-testbench/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + com.vaadin + vaadin-breadcrumb-flow-parent + 25.0-SNAPSHOT + + vaadin-breadcrumb-testbench + jar + Vaadin Breadcrumb Testbench API + Vaadin Breadcrumb Testbench API + + + com.vaadin + vaadin-testbench-shared + provided + + + + + + + + attach-docs + + + with-docs + + + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + + + diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-testbench/src/main/java/com/vaadin/flow/component/breadcrumb/testbench/BreadcrumbElement.java b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-testbench/src/main/java/com/vaadin/flow/component/breadcrumb/testbench/BreadcrumbElement.java new file mode 100644 index 00000000000..56da9e41461 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-testbench/src/main/java/com/vaadin/flow/component/breadcrumb/testbench/BreadcrumbElement.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.breadcrumb.testbench; + +import java.util.List; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elementsbase.Element; + +/** + * TestBench element for the {@code } component. + * + * @author Vaadin Ltd. + */ +@Element("vaadin-breadcrumb") +public class BreadcrumbElement extends TestBenchElement { + + /** + * Gets all breadcrumb items in this breadcrumb. + * + * @return a list of all breadcrumb items + */ + public List getItems() { + return $(BreadcrumbItemElement.class).all(); + } + + /** + * Gets the breadcrumb item at the given index. + * + * @param index + * the index of the item to get + * @return the breadcrumb item at the given index + */ + public BreadcrumbItemElement getItem(int index) { + return $(BreadcrumbItemElement.class).get(index); + } + + /** + * Gets the number of breadcrumb items. + * + * @return the number of items + */ + public int getItemCount() { + return getItems().size(); + } + + /** + * Clicks on the breadcrumb item at the given index. + * + * @param index + * the index of the item to click + */ + public void clickItem(int index) { + getItem(index).click(); + } + + /** + * Gets the text of the breadcrumb item at the given index. + * + * @param index + * the index of the item + * @return the text of the item + */ + public String getItemText(int index) { + return getItem(index).getText(); + } + + /** + * Checks if the breadcrumb has the given theme variant. + * + * @param variant + * the theme variant to check + * @return {@code true} if the breadcrumb has the variant, {@code false} + * otherwise + */ + public boolean hasThemeVariant(String variant) { + String theme = getAttribute("theme"); + return theme != null && theme.contains(variant); + } +} diff --git a/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-testbench/src/main/java/com/vaadin/flow/component/breadcrumb/testbench/BreadcrumbItemElement.java b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-testbench/src/main/java/com/vaadin/flow/component/breadcrumb/testbench/BreadcrumbItemElement.java new file mode 100644 index 00000000000..6c1125397a5 --- /dev/null +++ b/vaadin-breadcrumb-flow-parent/vaadin-breadcrumb-testbench/src/main/java/com/vaadin/flow/component/breadcrumb/testbench/BreadcrumbItemElement.java @@ -0,0 +1,114 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.breadcrumb.testbench; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elementsbase.Element; + +/** + * TestBench element for the {@code } component. + * + * @author Vaadin Ltd. + */ +@Element("vaadin-breadcrumb-item") +public class BreadcrumbItemElement extends TestBenchElement { + + /** + * Gets the href of this breadcrumb item. + * + * @return the href, or {@code null} if not set + */ + public String getHref() { + return getPropertyString("href"); + } + + /** + * Gets the target of this breadcrumb item. + * + * @return the target, or {@code null} if not set + */ + public String getTarget() { + return getPropertyString("target"); + } + + /** + * Checks if this breadcrumb item is marked as the current page. + * + * @return {@code true} if this is the current page, {@code false} otherwise + */ + public boolean isCurrent() { + return hasAttribute("current"); + } + + /** + * Checks if this breadcrumb item is the last item in the breadcrumb. + * + * @return {@code true} if this is the last item, {@code false} otherwise + */ + public boolean isLast() { + return hasAttribute("last"); + } + + /** + * Checks if this breadcrumb item is disabled. + * + * @return {@code true} if disabled, {@code false} otherwise + */ + public boolean isDisabled() { + return hasAttribute("disabled"); + } + + /** + * Checks if this breadcrumb item should be ignored by client-side routers. + * + * @return {@code true} if router should ignore this item, {@code false} + * otherwise + */ + public boolean isRouterIgnore() { + return getPropertyBoolean("routerIgnore"); + } + + /** + * Gets the tooltip text of this breadcrumb item. + * + * @return the tooltip text, or {@code null} if not set + */ + public String getTooltipText() { + return getAttribute("title"); + } + + /** + * Clicks on the link part of this breadcrumb item. This will navigate to + * the href if one is set and the item is not disabled. + */ + public void clickLink() { + // Click on the link part in the shadow DOM + getCommandExecutor().executeScript( + "arguments[0].shadowRoot.querySelector('[part=\"link\"]').click();", + this); + } + + /** + * Checks if this breadcrumb item has a separator. + * + * @return {@code true} if a separator is visible, {@code false} otherwise + */ + public boolean hasSeparator() { + return (Boolean) getCommandExecutor().executeScript( + "return !!arguments[0].shadowRoot.querySelector('[part=\"separator\"]');", + this); + } +}