diff --git a/flow-components-bom/pom.xml b/flow-components-bom/pom.xml index dd7f76b1040..32f9b96152a 100644 --- a/flow-components-bom/pom.xml +++ b/flow-components-bom/pom.xml @@ -407,6 +407,16 @@ vaadin-side-nav-testbench ${project.version} + + com.vaadin + vaadin-stepper-flow + ${project.version} + + + com.vaadin + vaadin-stepper-testbench + ${project.version} + com.vaadin vaadin-split-layout-flow diff --git a/pom.xml b/pom.xml index 743624eb407..8756ee79e15 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,7 @@ vaadin-spreadsheet-flow-parent vaadin-field-highlighter-flow-parent vaadin-side-nav-flow-parent + vaadin-stepper-flow-parent vaadin-master-detail-layout-flow-parent diff --git a/vaadin-stepper-flow-parent/pom.xml b/vaadin-stepper-flow-parent/pom.xml new file mode 100644 index 00000000000..cd578db3f83 --- /dev/null +++ b/vaadin-stepper-flow-parent/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + com.vaadin + vaadin-flow-components + 25.0-SNAPSHOT + + vaadin-stepper-flow-parent + pom + Vaadin Stepper Parent + Vaadin Stepper Parent + + vaadin-stepper-flow + vaadin-stepper-testbench + + + + + default + + + !release + + + + vaadin-stepper-flow-integration-tests + + + + \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow-integration-tests/pom.xml b/vaadin-stepper-flow-parent/vaadin-stepper-flow-integration-tests/pom.xml new file mode 100644 index 00000000000..2be1f1f210f --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow-integration-tests/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + com.vaadin + vaadin-stepper-flow-parent + 25.0-SNAPSHOT + + vaadin-stepper-integration-tests + war + Vaadin Stepper Integration Tests + Vaadin Stepper 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-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-stepper-flow + ${project.version} + + + com.vaadin + vaadin-stepper-testbench + ${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 + + + + + + \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/pom.xml b/vaadin-stepper-flow-parent/vaadin-stepper-flow/pom.xml new file mode 100644 index 00000000000..7de7e01da32 --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + com.vaadin + vaadin-stepper-flow-parent + 25.0-SNAPSHOT + + vaadin-stepper-flow + jar + Vaadin Stepper + Vaadin Stepper + + + 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 + + + + + + \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Step.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Step.java new file mode 100644 index 00000000000..9bcb5d68be1 --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Step.java @@ -0,0 +1,498 @@ +/* + * 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.stepper; + +import java.util.Optional; + +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 step in a {@link Stepper} component. + *

+ * Step can contain text and/or components. It supports navigation through href + * property or by using Router navigation targets. Steps can have different + * states to indicate progress through the stepper process. + * + * @author Vaadin Ltd. + */ +@Tag("vaadin-step") +@JsModule("@vaadin/stepper/src/vaadin-step.js") +@NpmPackage(value = "@vaadin/stepper", version = "25.0.0-dev") +public class Step 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 labelDescriptor = PropertyDescriptors + .propertyWithDefault("label", ""); + + private static final PropertyDescriptor descriptionDescriptor = PropertyDescriptors + .propertyWithDefault("description", ""); + + private static final PropertyDescriptor stateDescriptor = PropertyDescriptors + .propertyWithDefault("state", State.INACTIVE.getValue()); + + private static final PropertyDescriptor routerIgnoreDescriptor = PropertyDescriptors + .propertyWithDefault("routerIgnore", false); + + /** + * Enumeration of possible step states. + */ + public enum State { + ACTIVE("active"), COMPLETED("completed"), ERROR("error"), INACTIVE( + "inactive"); + + private final String value; + + State(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static State fromValue(String value) { + for (State state : values()) { + if (state.getValue().equals(value)) { + return state; + } + } + return INACTIVE; + } + } + + /** + * Constructs an empty step. + */ + public Step() { + super(); + } + + /** + * Constructs a step with the given label. + * + * @param label + * the label text + */ + public Step(String label) { + this(); + setLabel(label); + } + + /** + * Constructs a step with the given label and description. + * + * @param label + * the label text + * @param description + * the description text + */ + public Step(String label, String description) { + this(label); + setDescription(description); + } + + /** + * Constructs a step with the given component. + * + * @param component + * the component to add + */ + public Step(Component component) { + this(); + add(component); + } + + /** + * Creates a step with the given label and href. + * + * @param label + * the label text + * @param href + * the href to navigate to + * @return a new step with the specified label and href + */ + public static Step withHref(String label, String href) { + Step step = new Step(label); + step.setHref(href); + return step; + } + + /** + * Creates a step with the given component and href. + * + * @param component + * the component to add + * @param href + * the href to navigate to + * @return a new step with the specified component and href + */ + public static Step withHref(Component component, String href) { + Step step = new Step(component); + step.setHref(href); + return step; + } + + /** + * Constructs a step with the given label and navigation target. + * + * @param label + * the label text + * @param navigationTarget + * the navigation target class + */ + public Step(String label, Class navigationTarget) { + this(label); + setRoute(navigationTarget); + } + + /** + * Constructs a step with the given label, navigation target, and route + * parameters. + * + * @param label + * the label text + * @param navigationTarget + * the navigation target class + * @param routeParameters + * the route parameters + */ + public Step(String label, Class navigationTarget, + RouteParameters routeParameters) { + this(label); + setRoute(navigationTarget, routeParameters); + } + + /** + * Gets the href of this step. + * + * @return the href, or an empty string if no href is set + */ + public String getHref() { + return get(hrefDescriptor); + } + + /** + * Sets the href of this step. + *

+ * The href is the URL that the step 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 step'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 step'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 the label of this step. + * + * @return the label text + */ + public String getLabel() { + return get(labelDescriptor); + } + + /** + * Sets the label of this step. + * + * @param label + * the label text to set + */ + public void setLabel(String label) { + set(labelDescriptor, label == null ? "" : label); + } + + /** + * Gets the description of this step. + * + * @return the description text + */ + public String getDescription() { + return get(descriptionDescriptor); + } + + /** + * Sets the description of this step. + * + * @param description + * the description text to set + */ + public void setDescription(String description) { + set(descriptionDescriptor, description == null ? "" : description); + } + + /** + * Gets the state of this step. + * + * @return the current state + */ + @Synchronize(property = "state", value = "state-changed") + public State getState() { + return State.fromValue(get(stateDescriptor)); + } + + /** + * Sets the state of this step. + * + * @param state + * the state to set + */ + public void setState(State state) { + set(stateDescriptor, state == null ? State.INACTIVE.getValue() + : state.getValue()); + } + + /** + * Gets whether this step should be ignored by client-side routers. + * + * @return {@code true} if router should ignore this step, {@code false} + * otherwise + */ + public boolean isRouterIgnore() { + return get(routerIgnoreDescriptor); + } + + /** + * Sets whether this step should be ignored by client-side routers. + *

+ * When set to {@code true}, clicking this step 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 step represents the current page. + *

+ * This property is automatically updated based on the current URL. + * + * @return {@code true} if this step represents the current page, + * {@code false} otherwise + */ + @Synchronize(property = "current", value = "current-changed") + public boolean isCurrent() { + return getElement().getProperty("current", false); + } + + /** + * Gets whether this step is completed. + * + * @return {@code true} if the step is completed, {@code false} otherwise + */ + public boolean isCompleted() { + return getState() == State.COMPLETED; + } + + /** + * Sets this step as completed. + */ + public void setCompleted() { + setState(State.COMPLETED); + } + + /** + * Gets whether this step is active. + * + * @return {@code true} if the step is active, {@code false} otherwise + */ + public boolean isActive() { + return getState() == State.ACTIVE; + } + + /** + * Sets this step as active. + */ + public void setActive() { + setState(State.ACTIVE); + } + + /** + * Gets whether this step has an error. + * + * @return {@code true} if the step has an error, {@code false} otherwise + */ + public boolean isError() { + return getState() == State.ERROR; + } + + /** + * Sets this step as having an error. + */ + public void setError() { + setState(State.ERROR); + } + + /** + * Gets whether this step is inactive. + * + * @return {@code true} if the step is inactive, {@code false} otherwise + */ + public boolean isInactive() { + return getState() == State.INACTIVE; + } + + /** + * Sets this step as inactive. + */ + public void setInactive() { + setState(State.INACTIVE); + } + + /** + * Sets the navigation target for this step 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 step 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 step 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 step 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; + } +} \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Stepper.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Stepper.java new file mode 100644 index 00000000000..6b6b6517e9d --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/Stepper.java @@ -0,0 +1,294 @@ +/* + * 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.stepper; + +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.PropertyDescriptor; +import com.vaadin.flow.component.PropertyDescriptors; +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; + +/** + * Stepper is a navigation component that displays progress through a sequence + * of logical steps. It guides users through a step-by-step process and allows + * navigation between steps. + *

+ * Use Stepper when you want to break a complex process into smaller, manageable + * steps and show the user their progress through the process. + *

+ * {@link Step} components can be added to this component with the + * {@link #add(Step...)} method or the {@link #Stepper(Step...)} constructor. + * + * @author Vaadin Ltd. + */ +@Tag("vaadin-stepper") +@JsModule("@vaadin/stepper/src/vaadin-stepper.js") +@NpmPackage(value = "@vaadin/stepper", version = "25.0.0-dev") +public class Stepper extends Component + implements HasSize, HasStyle, HasThemeVariant { + + private static final PropertyDescriptor orientationDescriptor = PropertyDescriptors + .propertyWithDefault("orientation", Orientation.VERTICAL.getValue()); + + /** + * The valid orientations for {@link Stepper} instances. + */ + public enum Orientation { + HORIZONTAL("horizontal"), VERTICAL("vertical"); + + private final String value; + + Orientation(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + /** + * Constructs an empty stepper component with vertical orientation. + */ + public Stepper() { + super(); + } + + /** + * Constructs a stepper component with the given steps. + * + * @param steps + * the steps to add + */ + public Stepper(Step... steps) { + this(); + add(steps); + } + + /** + * Gets the orientation of the stepper. + * + * @return the orientation + */ + public Orientation getOrientation() { + String value = get(orientationDescriptor); + for (Orientation orientation : Orientation.values()) { + if (orientation.getValue().equals(value)) { + return orientation; + } + } + return Orientation.VERTICAL; + } + + /** + * Sets the orientation of the stepper. + * + * @param orientation + * the orientation to set + */ + public void setOrientation(Orientation orientation) { + Objects.requireNonNull(orientation, "Orientation cannot be null"); + set(orientationDescriptor, orientation.getValue()); + } + + /** + * Adds the given steps to the component. + * + * @param steps + * the steps to add, not {@code null} + */ + public void add(Step... steps) { + Objects.requireNonNull(steps, "Steps to add cannot be null"); + Arrays.stream(steps).map(step -> Objects.requireNonNull(step, + "Individual step to add cannot be null")) + .map(Step::getElement) + .forEach(getElement()::appendChild); + } + + /** + * Removes the given steps from the component. + * + * @param steps + * the steps to remove, not {@code null} + */ + public void remove(Step... steps) { + Objects.requireNonNull(steps, "Steps to remove cannot be null"); + Arrays.stream(steps).map(step -> Objects.requireNonNull(step, + "Individual step to remove cannot be null")) + .map(Step::getElement) + .forEach(getElement()::removeChild); + } + + /** + * Removes all steps from the component. + */ + public void removeAll() { + getElement().removeAllChildren(); + } + + /** + * Gets the step at the given index. + * + * @param index + * the index of the step to get + * @return the step at the given index + * @throws IndexOutOfBoundsException + * if the index is out of range + */ + public Step getStepAt(int index) { + if (index < 0 || index >= getStepCount()) { + throw new IndexOutOfBoundsException("Index: " + index + + ", Size: " + getStepCount()); + } + Iterator iterator = getSteps().iterator(); + for (int i = 0; i < index; i++) { + iterator.next(); + } + return iterator.next(); + } + + /** + * Gets the number of steps in the component. + * + * @return the number of steps + */ + public int getStepCount() { + return (int) getSteps().count(); + } + + /** + * Gets the index of the given step. + * + * @param step + * the step to get the index of + * @return the index of the step, or -1 if not found + */ + public int indexOf(Step step) { + if (step == null) { + return -1; + } + Iterator iterator = getSteps().iterator(); + int index = 0; + while (iterator.hasNext()) { + if (iterator.next().equals(step)) { + return index; + } + index++; + } + return -1; + } + + /** + * Replaces the step at the given index with a new step. + * + * @param index + * the index of the step to replace + * @param newStep + * the new step to set, not {@code null} + * @throws IndexOutOfBoundsException + * if the index is out of range + */ + public void replace(int index, Step newStep) { + Objects.requireNonNull(newStep, "New step cannot be null"); + if (index < 0 || index >= getStepCount()) { + throw new IndexOutOfBoundsException("Index: " + index + + ", Size: " + getStepCount()); + } + + Step oldStep = getStepAt(index); + Element parentElement = getElement(); + List children = new ArrayList<>(); + parentElement.getChildren().forEach(children::add); + + int elementIndex = children.indexOf(oldStep.getElement()); + if (elementIndex >= 0) { + parentElement.insertChild(elementIndex, newStep.getElement()); + parentElement.removeChild(oldStep.getElement()); + } + } + + /** + * Gets all steps in the component as a stream. + * + * @return a stream of all steps + */ + public Stream getSteps() { + return getElement().getChildren() + .filter(element -> element.getComponent() + .filter(Step.class::isInstance).isPresent()) + .map(element -> element.getComponent().map(Step.class::cast) + .get()); + } + + /** + * Gets the currently active step. + * + * @return the active step, or {@code null} if no step is active + */ + public Step getActiveStep() { + return getSteps().filter(Step::isActive).findFirst().orElse(null); + } + + /** + * Sets the state of a specific step by index. + * + * @param state + * the state to set + * @param stepIndex + * the index of the step + */ + public void setStepState(Step.State state, int stepIndex) { + Objects.requireNonNull(state, "State cannot be null"); + if (stepIndex >= 0 && stepIndex < getStepCount()) { + getStepAt(stepIndex).setState(state); + } + } + + /** + * Marks steps as completed up to the specified index (inclusive). + * + * @param untilIndex + * the index up to which steps should be marked as completed + */ + public void completeStepsUntil(int untilIndex) { + if (untilIndex < 0) { + return; + } + int count = getStepCount(); + for (int i = 0; i <= untilIndex && i < count; i++) { + getStepAt(i).setState(Step.State.COMPLETED); + } + } + + /** + * Resets all steps to inactive state. + */ + public void reset() { + getSteps().forEach(step -> step.setState(Step.State.INACTIVE)); + } +} \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/StepperVariant.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/StepperVariant.java new file mode 100644 index 00000000000..450ae749ddf --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/main/java/com/vaadin/flow/component/stepper/StepperVariant.java @@ -0,0 +1,47 @@ +/* + * 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.stepper; + +import com.vaadin.flow.component.shared.ThemeVariant; + +/** + * Theme variants for the {@link Stepper} component. + * + * @author Vaadin Ltd. + */ +public enum StepperVariant implements ThemeVariant { + + /** + * Small size variant for compact display. + */ + LUMO_SMALL("small"); + + private final String variant; + + StepperVariant(String variant) { + this.variant = variant; + } + + /** + * Gets the variant name. + * + * @return variant name + */ + @Override + public String getVariantName() { + return variant; + } +} \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepTest.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepTest.java new file mode 100644 index 00000000000..8b022b45fcb --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepTest.java @@ -0,0 +1,356 @@ +/* + * 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.stepper; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.RouteParameters; + +/** + * Tests for {@link Step}. + */ +public class StepTest { + + @Test + public void createEmptyStep() { + Step step = new Step(); + + Assert.assertEquals("Tag name is invalid", "vaadin-step", + step.getElement().getTag()); + Assert.assertEquals("Label should be empty", "", step.getLabel()); + Assert.assertEquals("Description should be empty", "", + step.getDescription()); + Assert.assertEquals("Href should be empty", "", step.getHref()); + Assert.assertEquals("Default state should be inactive", + Step.State.INACTIVE, step.getState()); + } + + @Test + public void createStepWithLabel() { + Step step = new Step("Personal Info"); + + Assert.assertEquals("Label is invalid", "Personal Info", + step.getLabel()); + Assert.assertEquals("Href should be empty", "", step.getHref()); + } + + @Test + public void createStepWithLabelAndDescription() { + Step step = new Step("Personal Info", "Enter your details"); + + Assert.assertEquals("Label is invalid", "Personal Info", + step.getLabel()); + Assert.assertEquals("Description is invalid", "Enter your details", + step.getDescription()); + } + + @Test + public void createStepWithLabelAndHref() { + Step step = Step.withHref("Personal Info", "/personal"); + + Assert.assertEquals("Label is invalid", "Personal Info", + step.getLabel()); + Assert.assertEquals("Href is invalid", "/personal", step.getHref()); + } + + @Test + public void createStepWithComponent() { + Div div = new Div("Content"); + Step step = new Step(div); + + Assert.assertEquals("Child count is invalid", 1, + step.getElement().getChildCount()); + Assert.assertEquals("Child component is invalid", div.getElement(), + step.getElement().getChild(0)); + } + + @Test + public void createStepWithComponentAndHref() { + Div div = new Div("Content"); + Step step = Step.withHref(div, "/step"); + + Assert.assertEquals("Child count is invalid", 1, + step.getElement().getChildCount()); + Assert.assertEquals("Href is invalid", "/step", step.getHref()); + } + + @Test + public void setHref() { + Step step = new Step(); + + step.setHref("/products"); + Assert.assertEquals("Href is invalid", "/products", step.getHref()); + + step.setHref(null); + Assert.assertEquals("Href should be empty", "", step.getHref()); + + step.setHref(""); + Assert.assertEquals("Href should be empty", "", step.getHref()); + } + + @Test + public void setTarget() { + Step step = new Step(); + + step.setTarget("_blank"); + Assert.assertEquals("Target is invalid", "_blank", step.getTarget()); + + step.setTarget(null); + Assert.assertEquals("Target should be empty", "", step.getTarget()); + + step.setTarget(""); + Assert.assertEquals("Target should be empty", "", step.getTarget()); + } + + @Test + public void setLabel() { + Step step = new Step(); + + step.setLabel("Payment"); + Assert.assertEquals("Label is invalid", "Payment", step.getLabel()); + + step.setLabel(null); + Assert.assertEquals("Label should be empty", "", step.getLabel()); + } + + @Test + public void setDescription() { + Step step = new Step(); + + step.setDescription("Choose payment method"); + Assert.assertEquals("Description is invalid", "Choose payment method", + step.getDescription()); + + step.setDescription(null); + Assert.assertEquals("Description should be empty", "", + step.getDescription()); + } + + @Test + public void setState() { + Step step = new Step(); + + Assert.assertEquals("Default state should be inactive", + Step.State.INACTIVE, step.getState()); + + step.setState(Step.State.ACTIVE); + Assert.assertEquals("State should be active", Step.State.ACTIVE, + step.getState()); + + step.setState(Step.State.COMPLETED); + Assert.assertEquals("State should be completed", Step.State.COMPLETED, + step.getState()); + + step.setState(Step.State.ERROR); + Assert.assertEquals("State should be error", Step.State.ERROR, + step.getState()); + + step.setState(null); + Assert.assertEquals("State should be inactive", Step.State.INACTIVE, + step.getState()); + } + + @Test + public void stateConvenienceMethods() { + Step step = new Step(); + + // Test isInactive and setInactive + Assert.assertTrue("Step should be inactive by default", + step.isInactive()); + step.setInactive(); + Assert.assertTrue("Step should be inactive", step.isInactive()); + Assert.assertFalse("Step should not be active", step.isActive()); + Assert.assertFalse("Step should not be completed", step.isCompleted()); + Assert.assertFalse("Step should not be error", step.isError()); + + // Test isActive and setActive + step.setActive(); + Assert.assertTrue("Step should be active", step.isActive()); + Assert.assertFalse("Step should not be inactive", step.isInactive()); + Assert.assertFalse("Step should not be completed", step.isCompleted()); + Assert.assertFalse("Step should not be error", step.isError()); + + // Test isCompleted and setCompleted + step.setCompleted(); + Assert.assertTrue("Step should be completed", step.isCompleted()); + Assert.assertFalse("Step should not be inactive", step.isInactive()); + Assert.assertFalse("Step should not be active", step.isActive()); + Assert.assertFalse("Step should not be error", step.isError()); + + // Test isError and setError + step.setError(); + Assert.assertTrue("Step should be error", step.isError()); + Assert.assertFalse("Step should not be inactive", step.isInactive()); + Assert.assertFalse("Step should not be active", step.isActive()); + Assert.assertFalse("Step should not be completed", step.isCompleted()); + } + + @Test + public void setRouterIgnore() { + Step step = new Step(); + + Assert.assertFalse("Default routerIgnore should be false", + step.isRouterIgnore()); + + step.setRouterIgnore(true); + Assert.assertTrue("RouterIgnore should be true", step.isRouterIgnore()); + + step.setRouterIgnore(false); + Assert.assertFalse("RouterIgnore should be false", + step.isRouterIgnore()); + } + + @Test + public void setText() { + Step step = new Step(); + + step.setText("Step Content"); + Assert.assertEquals("Text is invalid", "Step Content", step.getText()); + + step.setText(null); + Assert.assertEquals("Text should be empty", "", step.getText()); + } + + @Test + public void addComponent() { + Step step = new Step(); + Div div1 = new Div("First"); + Div div2 = new Div("Second"); + + step.add(div1, div2); + + Assert.assertEquals("Child count is invalid", 2, + step.getElement().getChildCount()); + Assert.assertEquals("First child is invalid", div1.getElement(), + step.getElement().getChild(0)); + Assert.assertEquals("Second child is invalid", div2.getElement(), + step.getElement().getChild(1)); + } + + @Test + public void removeComponent() { + Step step = new Step(); + Div div1 = new Div("First"); + Div div2 = new Div("Second"); + + step.add(div1, div2); + step.remove(div1); + + Assert.assertEquals("Child count is invalid", 1, + step.getElement().getChildCount()); + Assert.assertEquals("Remaining child is invalid", div2.getElement(), + step.getElement().getChild(0)); + } + + @Test + public void removeAllComponents() { + Step step = new Step(); + Div div1 = new Div("First"); + Div div2 = new Div("Second"); + + step.add(div1, div2); + step.removeAll(); + + Assert.assertEquals("Child count should be 0", 0, + step.getElement().getChildCount()); + } + + @Test + public void setEnabled() { + Step step = new Step(); + + Assert.assertTrue("Default enabled should be true", step.isEnabled()); + + step.setEnabled(false); + Assert.assertFalse("Enabled should be false", step.isEnabled()); + + step.setEnabled(true); + Assert.assertTrue("Enabled should be true", step.isEnabled()); + } + + @Test + public void setTooltip() { + Step step = new Step(); + + // HasTooltip creates a tooltip immediately, so we check if text is + // null/empty + Assert.assertTrue("Default tooltip text should be null or empty", + step.getTooltip() == null || step.getTooltip().getText() == null + || step.getTooltip().getText().isEmpty()); + + step.setTooltipText("This is a tooltip"); + Assert.assertNotNull("Tooltip should not be null", step.getTooltip()); + Assert.assertEquals("Tooltip text is invalid", "This is a tooltip", + step.getTooltip().getText()); + } + + @Test + public void stateFromValue() { + Assert.assertEquals("Active state conversion", Step.State.ACTIVE, + Step.State.fromValue("active")); + Assert.assertEquals("Completed state conversion", Step.State.COMPLETED, + Step.State.fromValue("completed")); + Assert.assertEquals("Error state conversion", Step.State.ERROR, + Step.State.fromValue("error")); + Assert.assertEquals("Inactive state conversion", Step.State.INACTIVE, + Step.State.fromValue("inactive")); + Assert.assertEquals("Unknown state conversion", Step.State.INACTIVE, + Step.State.fromValue("unknown")); + Assert.assertEquals("Null state conversion", Step.State.INACTIVE, + Step.State.fromValue(null)); + } + + @Test + public void constructorsWithNavigationTarget() { + // Test constructor with navigation target - will throw without router + try { + Step step1 = new Step("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(); + Step step2 = new Step("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 { + } +} \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepperTest.java b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepperTest.java new file mode 100644 index 00000000000..8ecafc007c1 --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-flow/src/test/java/com/vaadin/flow/component/stepper/StepperTest.java @@ -0,0 +1,386 @@ +/* + * 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.stepper; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * Tests for {@link Stepper}. + */ +public class StepperTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createStepperInDefaultState() { + Stepper stepper = new Stepper(); + + Assert.assertEquals("Initial step count is invalid", 0, + stepper.getStepCount()); + Assert.assertEquals("Tag name is invalid", "vaadin-stepper", + stepper.getElement().getTag()); + Assert.assertEquals("Default orientation is invalid", + Stepper.Orientation.VERTICAL, stepper.getOrientation()); + } + + @Test + public void createStepperWithSteps() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step step3 = new Step("Step 3"); + Stepper stepper = new Stepper(step1, step2, step3); + + Assert.assertEquals("Initial step count is invalid", 3, + stepper.getStepCount()); + Assert.assertEquals("First step is invalid", step1, + stepper.getStepAt(0)); + Assert.assertEquals("Second step is invalid", step2, + stepper.getStepAt(1)); + Assert.assertEquals("Third step is invalid", step3, + stepper.getStepAt(2)); + } + + @Test + public void setOrientation() { + Stepper stepper = new Stepper(); + + stepper.setOrientation(Stepper.Orientation.HORIZONTAL); + Assert.assertEquals("Orientation is invalid", + Stepper.Orientation.HORIZONTAL, stepper.getOrientation()); + + stepper.setOrientation(Stepper.Orientation.VERTICAL); + Assert.assertEquals("Orientation is invalid", + Stepper.Orientation.VERTICAL, stepper.getOrientation()); + } + + @Test + public void setOrientationNull_throwsException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("Orientation cannot be null"); + + Stepper stepper = new Stepper(); + stepper.setOrientation(null); + } + + @Test + public void addSteps() { + Stepper stepper = new Stepper(); + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + + stepper.add(step1, step2); + + Assert.assertEquals("Step count after add is invalid", 2, + stepper.getStepCount()); + Assert.assertEquals("First step is invalid", step1, + stepper.getStepAt(0)); + Assert.assertEquals("Second step is invalid", step2, + stepper.getStepAt(1)); + } + + @Test + public void addNullSteps_throwsException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("Steps to add cannot be null"); + + Stepper stepper = new Stepper(); + stepper.add((Step[]) null); + } + + @Test + public void addStepWithNull_throwsException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("Individual step to add cannot be null"); + + Stepper stepper = new Stepper(); + stepper.add(new Step("Step 1"), null); + } + + @Test + public void removeSteps() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step step3 = new Step("Step 3"); + Stepper stepper = new Stepper(step1, step2, step3); + + stepper.remove(step2); + + Assert.assertEquals("Step count after remove is invalid", 2, + stepper.getStepCount()); + Assert.assertEquals("First step is invalid", step1, + stepper.getStepAt(0)); + Assert.assertEquals("Second step is invalid", step3, + stepper.getStepAt(1)); + } + + @Test + public void removeAll() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Stepper stepper = new Stepper(step1, step2); + + stepper.removeAll(); + + Assert.assertEquals("Step count after removeAll is invalid", 0, + stepper.getStepCount()); + } + + @Test + public void getStepAt_invalidIndex_throwsException() { + thrown.expect(IndexOutOfBoundsException.class); + thrown.expectMessage("Index: 0, Size: 0"); + + Stepper stepper = new Stepper(); + stepper.getStepAt(0); + } + + @Test + public void getStepAt_negativeIndex_throwsException() { + thrown.expect(IndexOutOfBoundsException.class); + thrown.expectMessage("Index: -1, Size: 0"); + + Stepper stepper = new Stepper(); + stepper.getStepAt(-1); + } + + @Test + public void indexOf_returnsCorrectIndex() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step step3 = new Step("Step 3"); + Stepper stepper = new Stepper(step1, step2, step3); + + Assert.assertEquals("Index of step1 is invalid", 0, + stepper.indexOf(step1)); + Assert.assertEquals("Index of step2 is invalid", 1, + stepper.indexOf(step2)); + Assert.assertEquals("Index of step3 is invalid", 2, + stepper.indexOf(step3)); + } + + @Test + public void indexOf_stepNotFound_returnsNegativeOne() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step stepNotAdded = new Step("Step 3"); + Stepper stepper = new Stepper(step1, step2); + + Assert.assertEquals("Index of not added step should be -1", -1, + stepper.indexOf(stepNotAdded)); + } + + @Test + public void indexOf_nullStep_returnsNegativeOne() { + Step step1 = new Step("Step 1"); + Stepper stepper = new Stepper(step1); + + Assert.assertEquals("Index of null should be -1", -1, + stepper.indexOf(null)); + } + + @Test + public void replace_validIndex() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step step3 = new Step("Step 3"); + Step newStep = new Step("New Step"); + Stepper stepper = new Stepper(step1, step2, step3); + + stepper.replace(1, newStep); + + Assert.assertEquals("Step count after replace is invalid", 3, + stepper.getStepCount()); + Assert.assertEquals("First step is invalid", step1, + stepper.getStepAt(0)); + Assert.assertEquals("Replaced step is invalid", newStep, + stepper.getStepAt(1)); + Assert.assertEquals("Third step is invalid", step3, + stepper.getStepAt(2)); + } + + @Test + public void replace_invalidIndex_throwsException() { + thrown.expect(IndexOutOfBoundsException.class); + thrown.expectMessage("Index: 3, Size: 2"); + + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step newStep = new Step("New Step"); + Stepper stepper = new Stepper(step1, step2); + + stepper.replace(3, newStep); + } + + @Test + public void replace_nullStep_throwsException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("New step cannot be null"); + + Step step1 = new Step("Step 1"); + Stepper stepper = new Stepper(step1); + + stepper.replace(0, null); + } + + @Test + public void getSteps_returnsAllSteps() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step step3 = new Step("Step 3"); + Stepper stepper = new Stepper(step1, step2, step3); + + Step[] steps = stepper.getSteps().toArray(Step[]::new); + + Assert.assertEquals("Step count is invalid", 3, steps.length); + Assert.assertEquals("First step is invalid", step1, steps[0]); + Assert.assertEquals("Second step is invalid", step2, steps[1]); + Assert.assertEquals("Third step is invalid", step3, steps[2]); + } + + @Test + public void getActiveStep() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step step3 = new Step("Step 3"); + Stepper stepper = new Stepper(step1, step2, step3); + + Assert.assertNull("Initially no step should be active", + stepper.getActiveStep()); + + step2.setState(Step.State.ACTIVE); + Assert.assertEquals("Active step should be step2", step2, + stepper.getActiveStep()); + } + + @Test + public void setStepState() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Stepper stepper = new Stepper(step1, step2); + + stepper.setStepState(Step.State.COMPLETED, 0); + Assert.assertEquals("Step state should be completed", + Step.State.COMPLETED, step1.getState()); + + stepper.setStepState(Step.State.ACTIVE, 1); + Assert.assertEquals("Step state should be active", Step.State.ACTIVE, + step2.getState()); + } + + @Test + public void setStepState_nullState_throwsException() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("State cannot be null"); + + Step step1 = new Step("Step 1"); + Stepper stepper = new Stepper(step1); + + stepper.setStepState(null, 0); + } + + @Test + public void setStepState_invalidIndex_ignored() { + Step step1 = new Step("Step 1"); + Stepper stepper = new Stepper(step1); + + // Should not throw exception, just ignore + stepper.setStepState(Step.State.COMPLETED, 5); + stepper.setStepState(Step.State.COMPLETED, -1); + + Assert.assertEquals("Step state should remain inactive", + Step.State.INACTIVE, step1.getState()); + } + + @Test + public void completeStepsUntil() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step step3 = new Step("Step 3"); + Stepper stepper = new Stepper(step1, step2, step3); + + stepper.completeStepsUntil(1); + + Assert.assertEquals("Step 1 should be completed", Step.State.COMPLETED, + step1.getState()); + Assert.assertEquals("Step 2 should be completed", Step.State.COMPLETED, + step2.getState()); + Assert.assertEquals("Step 3 should remain inactive", + Step.State.INACTIVE, step3.getState()); + } + + @Test + public void completeStepsUntil_negativeIndex_ignored() { + Step step1 = new Step("Step 1"); + Stepper stepper = new Stepper(step1); + + stepper.completeStepsUntil(-1); + + Assert.assertEquals("Step should remain inactive", Step.State.INACTIVE, + step1.getState()); + } + + @Test + public void completeStepsUntil_indexBeyondRange() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Stepper stepper = new Stepper(step1, step2); + + stepper.completeStepsUntil(5); + + Assert.assertEquals("Step 1 should be completed", Step.State.COMPLETED, + step1.getState()); + Assert.assertEquals("Step 2 should be completed", Step.State.COMPLETED, + step2.getState()); + } + + @Test + public void reset() { + Step step1 = new Step("Step 1"); + Step step2 = new Step("Step 2"); + Step step3 = new Step("Step 3"); + Stepper stepper = new Stepper(step1, step2, step3); + + step1.setState(Step.State.COMPLETED); + step2.setState(Step.State.ACTIVE); + step3.setState(Step.State.ERROR); + + stepper.reset(); + + Assert.assertEquals("Step 1 should be inactive", Step.State.INACTIVE, + step1.getState()); + Assert.assertEquals("Step 2 should be inactive", Step.State.INACTIVE, + step2.getState()); + Assert.assertEquals("Step 3 should be inactive", Step.State.INACTIVE, + step3.getState()); + } + + @Test + public void themeVariants() { + Stepper stepper = new Stepper(); + + stepper.addThemeVariants(StepperVariant.LUMO_SMALL); + Assert.assertTrue("Should have small variant", + stepper.getThemeNames().contains("small")); + + stepper.removeThemeVariants(StepperVariant.LUMO_SMALL); + Assert.assertFalse("Should not have small variant", + stepper.getThemeNames().contains("small")); + } +} \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-testbench/pom.xml b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/pom.xml new file mode 100644 index 00000000000..5cdbfcd8a6e --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + com.vaadin + vaadin-stepper-flow-parent + 25.0-SNAPSHOT + + vaadin-stepper-testbench + jar + Vaadin Stepper Testbench API + Vaadin Stepper 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 + + + + + + \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepElement.java b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepElement.java new file mode 100644 index 00000000000..1e0237ed792 --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepElement.java @@ -0,0 +1,206 @@ +/* + * 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.stepper.testbench; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elementsbase.Element; + +/** + * TestBench element for the {@code } component. + * + * @author Vaadin Ltd. + */ +@Element("vaadin-step") +public class StepElement extends TestBenchElement { + + /** + * Gets the href of this step. + * + * @return the href, or {@code null} if not set + */ + public String getHref() { + return getPropertyString("href"); + } + + /** + * Gets the target of this step. + * + * @return the target, or {@code null} if not set + */ + public String getTarget() { + return getPropertyString("target"); + } + + /** + * Gets the label of this step. + * + * @return the label text + */ + public String getLabel() { + return getPropertyString("label"); + } + + /** + * Gets the description of this step. + * + * @return the description text + */ + public String getDescription() { + return getPropertyString("description"); + } + + /** + * Gets the state of this step. + * + * @return the state ("active", "completed", "error", or "inactive") + */ + public String getState() { + return getPropertyString("state"); + } + + /** + * Checks if this step 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 step is disabled. + * + * @return {@code true} if disabled, {@code false} otherwise + */ + public boolean isDisabled() { + return hasAttribute("disabled"); + } + + /** + * Checks if this step should be ignored by client-side routers. + * + * @return {@code true} if router should ignore this step, {@code false} + * otherwise + */ + public boolean isRouterIgnore() { + return getPropertyBoolean("routerIgnore"); + } + + /** + * Checks if this step is in active state. + * + * @return {@code true} if the step is active, {@code false} otherwise + */ + public boolean isActive() { + return "active".equals(getState()); + } + + /** + * Checks if this step is in completed state. + * + * @return {@code true} if the step is completed, {@code false} otherwise + */ + public boolean isCompleted() { + return "completed".equals(getState()); + } + + /** + * Checks if this step is in error state. + * + * @return {@code true} if the step is in error state, {@code false} + * otherwise + */ + public boolean isError() { + return "error".equals(getState()); + } + + /** + * Checks if this step is in inactive state. + * + * @return {@code true} if the step is inactive, {@code false} otherwise + */ + public boolean isInactive() { + return "inactive".equals(getState()); + } + + /** + * Gets the tooltip text of this step. + * + * @return the tooltip text, or {@code null} if not set + */ + public String getTooltipText() { + return getAttribute("title"); + } + + /** + * Clicks on the indicator part of this step. This will navigate to the + * href if one is set and the step is not disabled. + */ + public void clickIndicator() { + // Click on the indicator part in the shadow DOM + getCommandExecutor().executeScript( + "arguments[0].shadowRoot.querySelector('[part=\"indicator\"]').click();", + this); + } + + /** + * Checks if this step has a connector line to the next step. + * + * @return {@code true} if a connector is visible, {@code false} otherwise + */ + public boolean hasConnector() { + return (Boolean) getCommandExecutor().executeScript( + "return !!arguments[0].shadowRoot.querySelector('[part=\"connector\"]');", + this); + } + + /** + * Gets the text content of the step indicator (usually a number or icon). + * + * @return the indicator text content + */ + public String getIndicatorText() { + return (String) getCommandExecutor().executeScript( + "const indicator = arguments[0].shadowRoot.querySelector('[part=\"indicator\"]');" + + "return indicator ? indicator.textContent.trim() : '';", + this); + } + + /** + * Checks if the step indicator shows a checkmark (completed state). + * + * @return {@code true} if checkmark is visible, {@code false} otherwise + */ + public boolean hasCheckmark() { + return (Boolean) getCommandExecutor().executeScript( + "const indicator = arguments[0].shadowRoot.querySelector('[part=\"indicator\"]');" + + "return indicator && indicator.textContent.includes('✓');", + this); + } + + /** + * Checks if the step indicator shows an error mark (error state). + * + * @return {@code true} if error mark is visible, {@code false} otherwise + */ + public boolean hasErrorMark() { + return (Boolean) getCommandExecutor().executeScript( + "const indicator = arguments[0].shadowRoot.querySelector('[part=\"indicator\"]');" + + "return indicator && indicator.textContent.includes('!');", + this); + } +} \ No newline at end of file diff --git a/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepperElement.java b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepperElement.java new file mode 100644 index 00000000000..ed8db794e25 --- /dev/null +++ b/vaadin-stepper-flow-parent/vaadin-stepper-testbench/src/main/java/com/vaadin/flow/component/stepper/testbench/StepperElement.java @@ -0,0 +1,152 @@ +/* + * 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.stepper.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-stepper") +public class StepperElement extends TestBenchElement { + + /** + * Gets all steps in this stepper. + * + * @return a list of all steps + */ + public List getSteps() { + return $(StepElement.class).all(); + } + + /** + * Gets the step at the given index. + * + * @param index + * the index of the step to get + * @return the step at the given index + */ + public StepElement getStep(int index) { + return $(StepElement.class).get(index); + } + + /** + * Gets the number of steps. + * + * @return the number of steps + */ + public int getStepCount() { + return getSteps().size(); + } + + /** + * Clicks on the step at the given index. + * + * @param index + * the index of the step to click + */ + public void clickStep(int index) { + getStep(index).click(); + } + + /** + * Gets the label of the step at the given index. + * + * @param index + * the index of the step + * @return the label of the step + */ + public String getStepLabel(int index) { + return getStep(index).getLabel(); + } + + /** + * Gets the description of the step at the given index. + * + * @param index + * the index of the step + * @return the description of the step + */ + public String getStepDescription(int index) { + return getStep(index).getDescription(); + } + + /** + * Gets the state of the step at the given index. + * + * @param index + * the index of the step + * @return the state of the step + */ + public String getStepState(int index) { + return getStep(index).getState(); + } + + /** + * Gets the orientation of the stepper. + * + * @return the orientation ("horizontal" or "vertical") + */ + public String getOrientation() { + return getPropertyString("orientation"); + } + + /** + * Checks if the stepper has the given theme variant. + * + * @param variant + * the theme variant to check + * @return {@code true} if the stepper has the variant, {@code false} + * otherwise + */ + public boolean hasThemeVariant(String variant) { + String theme = getAttribute("theme"); + return theme != null && theme.contains(variant); + } + + /** + * Gets the currently active step. + * + * @return the active step, or {@code null} if no step is active + */ + public StepElement getActiveStep() { + return getSteps().stream().filter(StepElement::isActive).findFirst() + .orElse(null); + } + + /** + * Gets all completed steps. + * + * @return a list of completed steps + */ + public List getCompletedSteps() { + return getSteps().stream().filter(StepElement::isCompleted).toList(); + } + + /** + * Gets all steps in error state. + * + * @return a list of steps in error state + */ + public List getErrorSteps() { + return getSteps().stream().filter(StepElement::isError).toList(); + } +} \ No newline at end of file