diff --git a/browserless-test/junit6/src/main/java/com/vaadin/browserless/BrowserlessTest.java b/browserless-test/junit6/src/main/java/com/vaadin/browserless/BrowserlessTest.java new file mode 100644 index 000000000..0b251c9b6 --- /dev/null +++ b/browserless-test/junit6/src/main/java/com/vaadin/browserless/BrowserlessTest.java @@ -0,0 +1,98 @@ +/** + * Copyright (C) 2000-2026 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.browserless; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +/** + * Base JUnit 6 class for browserless tests. + * + * The class automatically scans classpath for routes and error views. + * Subclasses should typically restrict classpath scanning to a specific + * packages for faster bootstrap, by using {@link ViewPackages} annotation. If + * the annotation is not present a full classpath scan is performed + * + *
+ * {@code
+ * @ViewPackages(classes = {CartView.class, CheckoutView.class})
+ * class CartViewTest extends BrowserlessTest {
+ * }
+ *
+ * @ViewPackages(packages = {"com.example.shop.cart", "com.example.security"})
+ * class CartViewTest extends BrowserlessTest {
+ * }
+ *
+ * @ViewPackages(
+ *    classes = {CartView.class, CheckoutView.class},
+ *    packages = {"com.example.security"}
+ * )
+ * class CartViewTest extends BrowserlessTest {
+ * }
+ * 
+ * + * Set up of Vaadin environment is performed before each test by {@link + * #initVaadinEnvironment()} method, and will be executed before + * {@code @BeforeEach} methods defined in subclasses. At the same way, cleanup + * tasks operated by {@link #cleanVaadinEnvironment()} are executed after each + * test, and after all {@code @AfterEach} annotated methods in subclasses. + * + * Usually, it is not necessary to override {@link #initVaadinEnvironment()} or + * {@link #cleanVaadinEnvironment()} methods, but if this is done it is + * mandatory to add the {@code @BeforeEach} and {@code @AfterEach} annotations + * in the subclass, in order to have hooks handled by testing framework. + * + * A use case for overriding {@link #initVaadinEnvironment()} is to provide + * custom Flow service implementations supported by {@link + * com.vaadin.flow.di.Lookup} SPI. Implementations can be provided overriding + * {@link #initVaadinEnvironment()} and passing to super implementation the + * service classes that should be initialized during setup. + * + *
+ * {@code
+ * @BeforeEach
+ * @Override
+ * void initVaadinEnvironment() {
+ *     super.initVaadinEnvironment(CustomInstantiatorFactory.class);
+ * }
+ * }
+ * 
+ *

+ * To get a graphical ascii representation of the UI tree on failure add the + * annotation {@code @ExtendWith(TreeOnFailureExtension.class)} to the test + * class. + * + * @see ViewPackages + */ +public abstract class BrowserlessTest extends BaseBrowserlessTest + implements TesterWrappers { + + @BeforeEach + protected void initVaadinEnvironment() { + super.initVaadinEnvironment(); + } + + @AfterEach + @Override + protected void cleanVaadinEnvironment() { + super.cleanVaadinEnvironment(); + } + + @Override + protected final String testingEngine() { + return "JUnit 6"; + } +} diff --git a/browserless-test/junit6/src/main/java/com/vaadin/browserless/SpringBrowserlessTest.java b/browserless-test/junit6/src/main/java/com/vaadin/browserless/SpringBrowserlessTest.java new file mode 100644 index 000000000..53b4c25a6 --- /dev/null +++ b/browserless-test/junit6/src/main/java/com/vaadin/browserless/SpringBrowserlessTest.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2000-2026 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.browserless; + +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.vaadin.browserless.internal.MockVaadin; +import com.vaadin.browserless.mocks.MockSpringServlet; +import com.vaadin.browserless.mocks.MockedUI; +import com.vaadin.browserless.mocks.SpringSecurityRequestCustomizer; + +/** + * Base JUnit 6 class for browserless testing of applications based on Spring + * Framework. + * + * This class provides functionality of the Spring TestContext Framework, in + * addition to set up a mock Vaadin Spring environment, so that views and + * components built upon dependency injection and AOP can be correctly be + * handled during unit testing. + * + * Usually when unit testing a UI view it is not needed to bootstrap the whole + * application. Subclasses can therefore be annotated + * with @{@link org.springframework.test.context.ContextConfiguration} or other + * Spring Testing annotations to load only required component or to provide mock + * services implementations. + * + *

+ * {@code
+ * @ContextConfiguration(classes = ViewTestConfig.class)
+ * class ViewTest extends SpringBrowserlessTest {
+ *
+ * }
+ * @Configuration
+ * class ViewTestConfig {
+ *     @Bean
+ *     MyService myService() {
+ *         return new my MockMyService();
+ *     }
+ * }
+ * }
+ * 
+ */ +@ExtendWith({ SpringExtension.class }) +@TestExecutionListeners(listeners = BrowserlessTestSpringLookupInitializer.class, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS) +public abstract class SpringBrowserlessTest extends BrowserlessTest { + + @Autowired + private ApplicationContext applicationContext; + + @Override + protected Set> lookupServices() { + return Set.of(BrowserlessTestSpringLookupInitializer.class, + SpringSecurityRequestCustomizer.class); + } + + @BeforeEach + protected void initVaadinEnvironment() { + scanTesters(); + MockSpringServlet servlet = new MockSpringServlet(discoverRoutes(), + applicationContext, MockedUI::new); + MockVaadin.setup(MockedUI::new, servlet, lookupServices()); + } +} diff --git a/browserless-test/junit6/src/main/java/com/vaadin/browserless/TreeOnFailureExtension.java b/browserless-test/junit6/src/main/java/com/vaadin/browserless/TreeOnFailureExtension.java new file mode 100644 index 000000000..0ed082b4e --- /dev/null +++ b/browserless-test/junit6/src/main/java/com/vaadin/browserless/TreeOnFailureExtension.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2000-2026 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.browserless; + +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import com.vaadin.browserless.internal.PrettyPrintTree; +import com.vaadin.flow.component.UI; + +/** + * JUnit 6 extension that will collect and output the component tree for the + * failing test UI. + *

+ * This can help with identifying a problem that has happened in the test where + * a component is missing or has faulty data. + */ +public class TreeOnFailureExtension implements AfterTestExecutionCallback { + + @Override + public void afterTestExecution(ExtensionContext extensionContext) { + boolean testFailed = extensionContext.getExecutionException() + .isPresent(); + if (testFailed) { + final String prettyPrintTree = PrettyPrintTree.Companion + .ofVaadin(UI.getCurrent()).print(); + extensionContext.publishReportEntry("Test " + + extensionContext.getTestClass().get().getSimpleName() + + "::" + extensionContext.getTestMethod().get().getName() + + " failed with the tree:\n" + prettyPrintTree); + } + } +} diff --git a/browserless-test/junit6/src/test/java/com/example/SingleParam.java b/browserless-test/junit6/src/test/java/com/example/SingleParam.java new file mode 100644 index 000000000..084e0930d --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/SingleParam.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2026 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.example; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.BeforeEvent; +import com.vaadin.flow.router.HasUrlParameter; +import com.vaadin.flow.router.Route; + +@Route("param") +@Tag("div") +public class SingleParam extends Component implements HasUrlParameter { + public String parameter; + + @Override + public void setParameter(BeforeEvent event, String parameter) { + this.parameter = parameter; + } +} diff --git a/browserless-test/junit6/src/test/java/com/example/TemplatedParam.java b/browserless-test/junit6/src/test/java/com/example/TemplatedParam.java new file mode 100644 index 000000000..d729e16ad --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/TemplatedParam.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2026 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.example; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.Route; + +@Route("template/:param") +@Tag("div") +public class TemplatedParam extends Component implements BeforeEnterObserver { + public String parameter; + + @Override + public void beforeEnter(BeforeEnterEvent event) { + parameter = event.getRouteParameters().get("param").get(); + } +} diff --git a/browserless-test/junit6/src/test/java/com/example/base/ErrorView.java b/browserless-test/junit6/src/test/java/com/example/base/ErrorView.java new file mode 100644 index 000000000..1bc103d7e --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/base/ErrorView.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2000-2026 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.example.base; + +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.ErrorParameter; +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.NotFoundException; + +public class ErrorView extends VerticalLayout + implements HasErrorParameter { + @Override + public int setErrorParameter(BeforeEnterEvent event, + ErrorParameter parameter) { + if (parameter.getException() instanceof NotFoundException) { + throw (NotFoundException) parameter.getException(); + } + throw new RuntimeException(parameter.getCaughtException()); + } +} diff --git a/browserless-test/junit6/src/test/java/com/example/base/HelloWorldView.java b/browserless-test/junit6/src/test/java/com/example/base/HelloWorldView.java new file mode 100644 index 000000000..70f8cce9a --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/base/HelloWorldView.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2000-2026 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.example.base; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; + +@Route("helloworld") +public class HelloWorldView extends VerticalLayout { + public HelloWorldView() { + add(new Button("Hello, World!")); + } +} diff --git a/browserless-test/junit6/src/test/java/com/example/base/ParametrizedView.java b/browserless-test/junit6/src/test/java/com/example/base/ParametrizedView.java new file mode 100644 index 000000000..7b4f72cac --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/base/ParametrizedView.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2000-2026 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.example.base; + +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEvent; +import com.vaadin.flow.router.HasUrlParameter; +import com.vaadin.flow.router.QueryParameters; +import com.vaadin.flow.router.Route; + +@Route("params") +public class ParametrizedView extends VerticalLayout + implements HasUrlParameter { + + Integer parameter; + QueryParameters qp; + + @Override + public void setParameter(BeforeEvent event, Integer parameter) { + this.parameter = parameter; + qp = event.getLocation().getQueryParameters(); + } +} diff --git a/browserless-test/junit6/src/test/java/com/example/base/ParentView.java b/browserless-test/junit6/src/test/java/com/example/base/ParentView.java new file mode 100644 index 000000000..f4660c1f6 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/base/ParentView.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2000-2026 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.example.base; + +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.RoutePrefix; +import com.vaadin.flow.router.RouterLayout; + +@RoutePrefix("parent") +public class ParentView extends VerticalLayout implements RouterLayout { +} diff --git a/browserless-test/junit6/src/test/java/com/example/base/WelcomeView.java b/browserless-test/junit6/src/test/java/com/example/base/WelcomeView.java new file mode 100644 index 000000000..7e861b61b --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/base/WelcomeView.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2000-2026 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.example.base; + +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteAlias; +import com.vaadin.flow.server.PWA; + +@Route("welcome") +@RouteAlias("") +@PWA(name = "My Foo PWA", shortName = "Foo PWA") +public class WelcomeView extends VerticalLayout { + public WelcomeView() { + setWidth(null); + add(new Text("Welcome!")); + } +} diff --git a/browserless-test/junit6/src/test/java/com/example/base/child/ChildView.java b/browserless-test/junit6/src/test/java/com/example/base/child/ChildView.java new file mode 100644 index 000000000..39da9425e --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/base/child/ChildView.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2000-2026 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.example.base.child; + +import com.example.base.ParentView; + +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; + +@Route(value = "child", layout = ParentView.class) +public class ChildView extends VerticalLayout { + +} diff --git a/browserless-test/junit6/src/test/java/com/example/base/navigation/NavigationPostponeView.java b/browserless-test/junit6/src/test/java/com/example/base/navigation/NavigationPostponeView.java new file mode 100644 index 000000000..57c6e214f --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/base/navigation/NavigationPostponeView.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2000-2026 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.example.base.navigation; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeLeaveEvent; +import com.vaadin.flow.router.BeforeLeaveObserver; +import com.vaadin.flow.router.Route; + +@Route("navigation-postpone") +public class NavigationPostponeView extends VerticalLayout + implements BeforeLeaveObserver { + @Override + public void beforeLeave(BeforeLeaveEvent event) { + BeforeLeaveEvent.ContinueNavigationAction action = event.postpone(); + Dialog dialog = new Dialog(); + dialog.add(new Span( + "Are you sure you want to leave such a beautiful view?"), + new Button("Yes", ev -> { + action.proceed(); + dialog.close(); + }), new Button("No", ev -> dialog.close())); + dialog.open(); + } +} diff --git a/browserless-test/junit6/src/test/java/com/example/base/signals/SignalsView.java b/browserless-test/junit6/src/test/java/com/example/base/signals/SignalsView.java new file mode 100644 index 000000000..dc02fc217 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/example/base/signals/SignalsView.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2000-2026 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.example.base.signals; + +import java.util.concurrent.CompletableFuture; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.signals.Signal; +import com.vaadin.flow.signals.shared.SharedNumberSignal; + +@Route(value = "signals") +public class SignalsView extends Div { + + public final NativeButton incrementButton; + public final NativeButton quickBackgroundTaskButton; + public final NativeButton slowBackgroundTaskButton; + public final Span counter; + public final Span asyncCounter; + public final Span asyncWithDelayCounter; + public final SharedNumberSignal numberSignal; + public final SharedNumberSignal asyncNumberSignal; + public final SharedNumberSignal asyncWithDelayNumberSignal; + + public SignalsView() { + numberSignal = new SharedNumberSignal(); + + Signal computedSignal = numberSignal + .mapIntValue(counter -> "Counter: " + counter); + incrementButton = new NativeButton("Increment", + ev -> numberSignal.incrementBy(1.0)); + counter = new Span("Counter: -"); + counter.bindText(computedSignal); + + asyncNumberSignal = new SharedNumberSignal(); + Signal asyncComputedSignal = asyncNumberSignal + .mapIntValue(counter -> "Counter: " + counter); + asyncCounter = new Span("Counter: -"); + asyncCounter.bindText(asyncComputedSignal); + + asyncWithDelayNumberSignal = new SharedNumberSignal(); + asyncWithDelayCounter = new Span("Counter: -"); + + quickBackgroundTaskButton = new NativeButton("Quick background task", + event -> { + CompletableFuture.runAsync( + () -> asyncNumberSignal.incrementBy(10.0), + CompletableFuture.delayedExecutor(100, + java.util.concurrent.TimeUnit.MILLISECONDS)); + }); + + Signal.effect(asyncWithDelayCounter, () -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + asyncWithDelayCounter.setText("Counter: " + + asyncWithDelayNumberSignal.getAsInt() + " (delayed)"); + }); + slowBackgroundTaskButton = new NativeButton("Quick background task", + event -> CompletableFuture.runAsync(() -> { + asyncWithDelayNumberSignal.incrementBy(10.0); + })); + + add(incrementButton, quickBackgroundTaskButton, + slowBackgroundTaskButton, counter, asyncCounter, + asyncWithDelayCounter); + } +} diff --git a/browserless-test/junit6/src/test/java/com/testapp/MyRouteNotFoundError.java b/browserless-test/junit6/src/test/java/com/testapp/MyRouteNotFoundError.java new file mode 100644 index 000000000..804a8ddc7 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/testapp/MyRouteNotFoundError.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2026 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.testapp; + +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.ErrorParameter; +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.NotFoundException; + +/** + * Having an app-custom [NotFoundException] handler should not crash the mocking + * process: https://github.com/mvysny/karibu-testing/issues/50 + */ +public class MyRouteNotFoundError extends VerticalLayout + implements HasErrorParameter { + @Override + public int setErrorParameter(BeforeEnterEvent event, + ErrorParameter parameter) { + throw new RuntimeException(parameter.getException()); + } +} diff --git a/browserless-test/junit6/src/test/java/com/testapp/security/LoginView.java b/browserless-test/junit6/src/test/java/com/testapp/security/LoginView.java new file mode 100644 index 000000000..0ac9e37ae --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/testapp/security/LoginView.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2000-2026 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.testapp.security; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "login", registerAtStartup = false) +public class LoginView extends Component implements HasComponents { +} diff --git a/browserless-test/junit6/src/test/java/com/testapp/security/ProtectedView.java b/browserless-test/junit6/src/test/java/com/testapp/security/ProtectedView.java new file mode 100644 index 000000000..5d8204f72 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/testapp/security/ProtectedView.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2000-2026 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.testapp.security; + +import jakarta.annotation.security.PermitAll; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "", registerAtStartup = false) +@PermitAll +public class ProtectedView extends Component implements HasComponents { +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/BrowserlessBaseClassTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/BrowserlessBaseClassTest.java new file mode 100644 index 000000000..84f399550 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/BrowserlessBaseClassTest.java @@ -0,0 +1,125 @@ +/** + * Copyright (C) 2000-2026 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.browserless; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import com.example.SingleParam; +import com.example.TemplatedParam; +import com.example.base.WelcomeView; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.di.InstantiatorFactory; +import com.vaadin.flow.di.Lookup; +import com.vaadin.flow.router.RouteBaseData; +import com.vaadin.flow.server.VaadinRequest; +import com.vaadin.flow.server.VaadinResponse; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.VaadinSession; +import com.vaadin.flow.server.auth.MenuAccessControl; + +class BrowserlessBaseClassTest { + + @Nested + class TestMethodExecutionTest extends BrowserlessTest { + @Test + void extendingBaseClass_runTest_vaadinInstancesAvailable() { + Assertions.assertNotNull(UI.getCurrent(), + "Expecting current UI to be available, but was not"); + Assertions.assertNotNull(VaadinService.getCurrent(), + "Expecting VaadinService to be available up, but was not"); + Assertions.assertNotNull(VaadinRequest.getCurrent(), + "Expecting VaadinRequest to be available up, but was not"); + Assertions.assertNotNull(VaadinResponse.getCurrent(), + "Expecting VaadinResponse to be available up, but was not"); + Assertions.assertNotNull(VaadinSession.getCurrent(), + "Expecting VaadinSession to be available up, but was not"); + } + + @Test + void extendingBaseClass_runTest_defaultRouteActive() { + Assertions.assertInstanceOf(WelcomeView.class, getCurrentView(), + "Expecting default route to be active, but was not"); + } + + } + + @Nested + class DiscoverAllRoutesTest extends BrowserlessTest { + + @Test + void extendingBaseClass_runTest_routesAreDiscovered() { + Set> routes = VaadinService.getCurrent() + .getRouter().getRegistry().getRegisteredRoutes().stream() + .map(RouteBaseData::getNavigationTarget) + .collect(Collectors.toSet()); + Set> allViews = new HashSet<>( + TestRoutes.INSTANCE.getViews()); + allViews.add(SingleParam.class); + allViews.add(TemplatedParam.class); + Assertions.assertEquals(allViews.size(), routes.size()); + Assertions.assertTrue(routes.containsAll(allViews)); + } + } + + @Nested + class CustomLookupServicesTest extends BrowserlessTest { + + @Override + protected Set> lookupServices() { + return Set.of(TestCustomInstantiatorFactory.class); + } + + @Test + void customService_availableInLookup() { + Lookup lookup = VaadinService.getCurrent().getContext() + .getAttribute(Lookup.class); + Assertions.assertNotNull(lookup, + "Expecting Lookup to be initialized"); + + InstantiatorFactory service = lookup + .lookup(InstantiatorFactory.class); + Assertions.assertNotNull(service, + "Expecting service to be available through Lookup"); + Assertions.assertInstanceOf(TestCustomInstantiatorFactory.class, + service, + "Expecting service to be " + + TestCustomInstantiatorFactory.class + .getSimpleName() + + " but was " + service.getClass().getSimpleName()); + } + } + + @Nested + class MenuAccessControlTest extends BrowserlessTest { + + @Test + void menuAccessControl_instanceAvailable() { + MenuAccessControl menuAccessControl = VaadinService.getCurrent() + .getInstantiator().getMenuAccessControl(); + Assertions.assertNotNull(menuAccessControl, + "Expecting MenuAccessControl to be available"); + + } + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/BrowserlessNavigationTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/BrowserlessNavigationTest.java new file mode 100644 index 000000000..ff80f8905 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/BrowserlessNavigationTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2000-2026 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.browserless; + +import java.util.Collections; + +import com.example.SingleParam; +import com.example.TemplatedParam; +import com.example.base.HelloWorldView; +import com.example.base.WelcomeView; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.internal.MockRouteNotFoundError; + +@ViewPackages(packages = "com.example") +public class BrowserlessNavigationTest extends BrowserlessTest { + + @Test + public void getCurrentView_returnsExpectedView() { + Assertions.assertTrue(getCurrentView() instanceof WelcomeView, + "WelcomeView has the empty RouteAlias so it should be served as default view"); + + HelloWorldView helloWorldView = navigate(HelloWorldView.class); + + Assertions.assertTrue(getCurrentView().equals(helloWorldView), + "getCurrentView should return the same instance as gotten on navigation"); + } + + @Test + public void navigationWithLocation_checksGeneratedViewType() { + navigate("helloworld", HelloWorldView.class); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> navigate("welcome", HelloWorldView.class), + "Navigation to path not returning given class should throw"); + + } + + @Test + public void navigationToParameterView_noParameterGiven_throwsException() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> navigate(SingleParam.class), + "Illegal argument should be thrown for missing parameter"); + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> navigate("param", SingleParam.class), + "No matching route should be found for string without parameter"); + Assertions.assertTrue(exception.getMessage() + .contains("Navigation resulted in unexpected class")); + Assertions.assertTrue(exception.getMessage() + .contains(MockRouteNotFoundError.class.getName())); + } + + @Test + public void navigationToParametrisedView_returnsInstantiatedView() { + final String PARAMETER = "single"; + + final SingleParam single = navigate(SingleParam.class, PARAMETER); + Assertions.assertEquals(PARAMETER, single.parameter, + "View should contain given parameter"); + + final TemplatedParam param = navigate(TemplatedParam.class, + Collections.singletonMap("param", PARAMETER)); + Assertions.assertEquals(PARAMETER, param.parameter, + "Template parameter should be available."); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/BrowserlessShortcutTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/BrowserlessShortcutTest.java new file mode 100644 index 000000000..1dd0edc63 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/BrowserlessShortcutTest.java @@ -0,0 +1,140 @@ +/** + * Copyright (C) 2000-2026 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.browserless; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.vaadin.flow.component.ClickNotifier; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.KeyModifier; +import com.vaadin.flow.component.Shortcuts; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.UI; + +@ViewPackages(packages = "com.example") +@ExtendWith(TreeOnFailureExtension.class) +class BrowserlessShortcutTest extends BrowserlessTest { + + @Test + void fireShortcut_UIListener_invokedForExactMatch() { + AtomicInteger eventsCounter = new AtomicInteger(); + UI.getCurrent().addShortcutListener(eventsCounter::incrementAndGet, + Key.KEY_W, KeyModifier.ALT, KeyModifier.SHIFT); + + fireShortcut(Key.KEY_W); + Assertions.assertEquals(0, eventsCounter.get()); + + fireShortcut(Key.KEY_W, KeyModifier.ALT); + Assertions.assertEquals(0, eventsCounter.get()); + + fireShortcut(Key.KEY_W, KeyModifier.ALT, KeyModifier.SHIFT); + Assertions.assertEquals(1, eventsCounter.get()); + + fireShortcut(Key.KEY_W, KeyModifier.ALT, KeyModifier.SHIFT, + KeyModifier.CONTROL); + Assertions.assertEquals(1, eventsCounter.get()); + + fireShortcut(Key.KEY_W, KeyModifier.ALT, KeyModifier.SHIFT); + Assertions.assertEquals(2, eventsCounter.get()); + } + + @Test + void fireShortcut_nestedComponents_listenersInvoked() { + AtomicInteger buttonEvents = new AtomicInteger(); + AtomicInteger nestedButtonEvents = new AtomicInteger(); + + var button = new Button(); + button.addClickListener(event -> buttonEvents.incrementAndGet()); + button.addClickShortcut(Key.ENTER); + Shortcuts.addShortcutListener(button, buttonEvents::incrementAndGet, + Key.KEY_G, KeyModifier.CONTROL); + + var nested = new Button(); + nested.addClickListener(event -> nestedButtonEvents.incrementAndGet()); + nested.addClickShortcut(Key.KEY_S, KeyModifier.ALT); + Shortcuts.addShortcutListener(nested, + nestedButtonEvents::incrementAndGet, Key.KEY_G, + KeyModifier.CONTROL); + + getCurrentView().getElement().appendChild(button.getElement(), + new Div(new Div(nested)).getElement()); + + fireShortcut(Key.ENTER); + Assertions.assertEquals(1, buttonEvents.get()); + Assertions.assertEquals(0, nestedButtonEvents.get()); + + fireShortcut(Key.KEY_S, KeyModifier.ALT); + Assertions.assertEquals(1, buttonEvents.get()); + Assertions.assertEquals(1, nestedButtonEvents.get()); + + fireShortcut(Key.KEY_G, KeyModifier.CONTROL); + Assertions.assertEquals(2, buttonEvents.get()); + Assertions.assertEquals(2, nestedButtonEvents.get()); + } + + @Test + void fireShortcut_modal_listenersInvokedOnModalChildren() { + AtomicInteger buttonEvents = new AtomicInteger(); + AtomicInteger modalButtonEvents = new AtomicInteger(); + + var button = new Button(); + button.addClickListener(event -> buttonEvents.incrementAndGet()); + button.addClickShortcut(Key.ENTER); + + var nested = new Button(); + nested.addClickListener(event -> modalButtonEvents.incrementAndGet()); + nested.addClickShortcut(Key.ENTER); + + Div modal = new Div(new Div(nested)); + getCurrentView().getElement().appendChild(button.getElement()); + + fireShortcut(Key.ENTER); + Assertions.assertEquals(1, buttonEvents.get()); + Assertions.assertEquals(0, modalButtonEvents.get()); + + UI.getCurrent().addModal(modal); + + fireShortcut(Key.ENTER); + Assertions.assertEquals(1, buttonEvents.get()); + Assertions.assertEquals(1, modalButtonEvents.get()); + + UI.getCurrent().setChildComponentModal(modal, false); + + fireShortcut(Key.ENTER); + Assertions.assertEquals(2, buttonEvents.get()); + Assertions.assertEquals(1, modalButtonEvents.get()); + } + + @Tag("button") + private static class Button extends Component + implements ClickNotifier") + .withProperty("deceased", Person::getDeceased) + .withFunction("onClick", person -> { + person.setDeceased(!person.getDeceased()); + basicGrid.getListDataView().refreshItem(person); + }); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BeanGridTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BeanGridTesterTest.java new file mode 100644 index 000000000..65cf8e90c --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BeanGridTesterTest.java @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2000-2026 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.grid; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +public class BeanGridTesterTest extends BrowserlessTest { + + BeanGridView view; + GridTester, Person> grid_; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(BeanGridView.class); + + view = navigate(BeanGridView.class); + grid_ = test(view.beanGrid); + } + + @Test + void beanGrid_assertBeanColumns() { + Assertions.assertEquals(2, grid_.size()); + + Assertions.assertTrue(grid_.getSelected().isEmpty()); + + final int firstName = grid_.getColumnPosition("firstName"); + final int age = grid_.getColumnPosition("age"); + + Assertions.assertEquals(view.first.getFirstName(), + grid_.getCellText(0, firstName)); + Assertions.assertEquals(Integer.toString(view.first.getAge()), + grid_.getCellText(0, age)); + Assertions.assertEquals(view.first.getLastName(), + grid_.getCellText(0, grid_.getColumnPosition("lastName"))); + Assertions.assertEquals(view.first.getEmail(), + grid_.getCellText(0, grid_.getColumnPosition("email"))); + Assertions.assertEquals(view.first.getAddress().toString(), + grid_.getCellText(0, grid_.getColumnPosition("address"))); + + Assertions.assertEquals(view.second.getFirstName(), + grid_.getCellText(1, firstName)); + Assertions.assertEquals(Integer.toString(view.second.getAge()), + grid_.getCellText(1, age)); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BeanGridView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BeanGridView.java new file mode 100644 index 000000000..8616ae53b --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BeanGridView.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2026 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.grid; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "basic-grid", registerAtStartup = false) +public class BeanGridView extends Component implements HasComponents { + + Grid beanGrid = new Grid<>(Person.class); + Person first = Person.createTestPerson1(); + Person second = Person.createTestPerson2(); + + public BeanGridView() { + beanGrid.setItems(first, second); + + add(beanGrid); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/CheckBox.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/CheckBox.java new file mode 100644 index 000000000..2e94aeac6 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/CheckBox.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2000-2026 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.grid; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; + +@Tag("check-box") +public class CheckBox extends Component { + boolean checked; + + public CheckBox(boolean checked) { + this.checked = checked; + } + + public boolean isChecked() { + return checked; + } + + public void setChecked(boolean checked) { + this.checked = checked; + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/Country.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/Country.java new file mode 100644 index 000000000..390a08afd --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/Country.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2000-2026 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.grid; + +/** + * Copied from grid integration-tests. + */ +public enum Country { + + FINLAND("Finland"), + SWEDEN("Sweden"), + USA("USA"), + RUSSIA("Russia"), + NETHERLANDS("Netherlands"), + SOUTH_AFRICA("South Africa"); + + private String name; + + private Country(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/GetTextCellRendererTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/GetTextCellRendererTest.java new file mode 100644 index 000000000..98ffddf01 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/GetTextCellRendererTest.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2000-2026 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.grid; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class GetTextCellRendererTest extends BrowserlessTest { + + RendererGridView view; + GridTester, Person> grid_; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(RendererGridView.class); + + view = navigate(RendererGridView.class); + grid_ = test(view.grid); + } + + @Test + void getCellText_componentRenderer_getTextRecursively() { + Assertions.assertEquals( + String.format("%s%s%d", view.first.getFirstName(), + view.first.getLastName(), view.first.getAge()), + grid_.getCellText(0, 0)); + } + + @Test + void getCellText_renderNull_getsNull() { + Assertions.assertNull(grid_.getCellText(0, 1)); + } + + @Test + void getCellText_nonTextComponent_getsEmptyString() { + Assertions.assertTrue(grid_.getCellText(0, 2).isEmpty()); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/GridTesterSortTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/GridTesterSortTest.java new file mode 100644 index 000000000..87ea96683 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/GridTesterSortTest.java @@ -0,0 +1,270 @@ +/** + * Copyright (C) 2000-2026 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.grid; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.data.provider.SortDirection; +import com.vaadin.flow.data.provider.SortOrder; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class GridTesterSortTest extends BrowserlessTest { + + SortGridView view; + GridTester, Person> grid_; + GridTester, Person> beanGrid_; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(SortGridView.class); + + view = navigate(SortGridView.class); + grid_ = test(view.grid); + beanGrid_ = test(view.beanGrid); + } + + @Test + void isColumnSortableIndex_getColumnSortableState() { + Assertions.assertTrue(grid_.isColumnSortable(0), + "firstName colum should be sortable"); + Assertions.assertTrue(grid_.isColumnSortable(1), + "age colum should be sortable"); + Assertions.assertFalse(grid_.isColumnSortable(2), + "email colum should not be sortable"); + } + + @Test + void isColumnSortableProperty_getColumnSortableState() { + Assertions.assertTrue(beanGrid_.isColumnSortable("firstName"), + "firstName colum should be sortable"); + Assertions.assertTrue(beanGrid_.isColumnSortable("age"), + "age colum should be sortable"); + Assertions.assertFalse(beanGrid_.isColumnSortable("email"), + "email colum should not be sortable"); + } + + @Test + void isColumnSortable_invalidColumn_throws() { + Assertions.assertThrows(IndexOutOfBoundsException.class, + () -> grid_.isColumnSortable(-1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, + () -> grid_.isColumnSortable(10)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> grid_.isColumnSortable("email")); + Assertions.assertThrows(IllegalArgumentException.class, + () -> beanGrid_.isColumnSortable("notAProperty")); + } + + @Test + void getSortDirectionIndex_sortableColum_getsDirection() { + Assertions.assertNull(grid_.getSortDirection(0)); + + grid_.getComponent().sort(GridSortOrder + .desc(grid_.getComponent().getColumns().get(0)).build()); + Assertions.assertEquals(grid_.getSortDirection(0), + SortDirection.DESCENDING); + + grid_.getComponent().sort(GridSortOrder + .asc(grid_.getComponent().getColumns().get(0)).build()); + Assertions.assertEquals(grid_.getSortDirection(0), + SortDirection.ASCENDING); + } + + @Test + void getSortDirectionOproperty_sortableColum_getsDirection() { + Assertions.assertNull(beanGrid_.getSortDirection("firstName")); + + beanGrid_.getComponent() + .sort(GridSortOrder.desc( + beanGrid_.getComponent().getColumnByKey("firstName")) + .build()); + Assertions.assertEquals(beanGrid_.getSortDirection("firstName"), + SortDirection.DESCENDING); + + beanGrid_.getComponent() + .sort(GridSortOrder.asc( + beanGrid_.getComponent().getColumnByKey("firstName")) + .build()); + Assertions.assertEquals(beanGrid_.getSortDirection("firstName"), + SortDirection.ASCENDING); + } + + @Test + void getSortDirection_nonSortableColum_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> grid_.getSortDirection(2)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> beanGrid_.getSortDirection("email")); + } + + @Test + void getSortDirection_invalidColum_throws() { + Assertions.assertThrows(IndexOutOfBoundsException.class, + () -> grid_.getSortDirection(-1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, + () -> grid_.getSortDirection(10)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> grid_.getSortDirection("email")); + Assertions.assertThrows(IllegalArgumentException.class, + () -> beanGrid_.getSortDirection("notAProperty")); + } + + @Test + void sortByColumnIndex_sortableColumn_gridIsSorted() { + // Sort ASC + grid_.sortByColumn(1); + Assertions.assertIterableEquals( + List.of(view.second, view.third, view.first), + List.of(grid_.getRow(0), grid_.getRow(1), grid_.getRow(2))); + + // Sort DESC + grid_.sortByColumn(1); + Assertions.assertIterableEquals( + List.of(view.first, view.third, view.second), + List.of(grid_.getRow(0), grid_.getRow(1), grid_.getRow(2))); + + // Remove sort + grid_.sortByColumn(1); + Assertions.assertIterableEquals( + List.of(view.first, view.second, view.third), + List.of(grid_.getRow(0), grid_.getRow(1), grid_.getRow(2))); + + } + + @Test + void sortByColumnIndexAndDirection_sortableColumn_gridIsSorted() { + List>> sortEvents = new ArrayList<>(); + view.grid.addSortListener(ev -> sortEvents.add(ev.getSortOrder())); + Grid.Column columnToSort = view.grid.getColumns().get(1); + + // Should click 2 times to get descending order + grid_.sortByColumn(1, SortDirection.DESCENDING); + Assertions.assertEquals(2, sortEvents.size()); + List sorts = sortEvents.stream().flatMap( + s -> s.stream().filter(x -> columnToSort == x.getSorted())) + .map(SortOrder::getDirection).collect(Collectors.toList()); + + Assertions.assertIterableEquals( + List.of(SortDirection.ASCENDING, SortDirection.DESCENDING), + sorts); + + Assertions.assertIterableEquals( + List.of(view.first, view.third, view.second), + List.of(grid_.getRow(0), grid_.getRow(1), grid_.getRow(2))); + + } + + @Test + void sortByColumnProperty_sortableColumn_gridIsSorted() { + beanGrid_.sortByColumn("age"); + Assertions.assertIterableEquals( + List.of(view.second, view.third, view.first), + List.of(beanGrid_.getRow(0), beanGrid_.getRow(1), + beanGrid_.getRow(2))); + // Sort DESC + beanGrid_.sortByColumn("age"); + Assertions.assertIterableEquals( + List.of(view.first, view.third, view.second), + List.of(beanGrid_.getRow(0), beanGrid_.getRow(1), + beanGrid_.getRow(2))); + + // Remove sort + beanGrid_.sortByColumn("age"); + Assertions.assertIterableEquals( + List.of(view.first, view.second, view.third), + List.of(beanGrid_.getRow(0), beanGrid_.getRow(1), + beanGrid_.getRow(2))); + } + + @Test + void sortByColumnPropertyAndDirection_sortableColumn_gridIsSorted() { + List>> sortEvents = new ArrayList<>(); + view.beanGrid.addSortListener(ev -> sortEvents.add(ev.getSortOrder())); + Grid.Column columnToSort = view.beanGrid.getColumnByKey("age"); + + // Should click 2 times to get descending order + beanGrid_.sortByColumn("age", SortDirection.DESCENDING); + Assertions.assertEquals(2, sortEvents.size()); + List sorts = sortEvents.stream().flatMap( + s -> s.stream().filter(x -> columnToSort == x.getSorted())) + .map(SortOrder::getDirection).collect(Collectors.toList()); + + Assertions.assertIterableEquals( + List.of(view.first, view.third, view.second), + List.of(beanGrid_.getRow(0), beanGrid_.getRow(1), + beanGrid_.getRow(2))); + + sortEvents.clear(); + beanGrid_.sortByColumn("age", SortDirection.DESCENDING); + Assertions.assertTrue(sortEvents.isEmpty(), + "Sort direction already reached, should do nothing"); + } + + @Test + void sortByColumn_nonSortableColumn_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> grid_.sortByColumn(2)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> beanGrid_.sortByColumn("email")); + } + + @Test + void sortByColumn_multisort_gridIsSorted() { + view.first.setFirstName("T"); + view.first.setAge(20); + view.second.setFirstName("A"); + view.second.setAge(25); + view.third.setFirstName("G"); + view.third.setAge(25); + view.grid.setMultiSort(true); + + grid_.sortByColumn(0); + grid_.sortByColumn(1, SortDirection.DESCENDING); + Assertions.assertIterableEquals( + List.of(view.second, view.third, view.first), + List.of(grid_.getRow(0), grid_.getRow(1), grid_.getRow(2))); + + } + + @Test + void sortByColumn_multisort_append_gridIsSorted() { + view.grid.setMultiSort(true, Grid.MultiSortPriority.APPEND); + view.first.setFirstName("G"); + view.first.setAge(20); + view.second.setFirstName("G"); + view.second.setAge(25); + view.third.setFirstName("A"); + view.third.setAge(25); + view.grid.setMultiSort(true); + + grid_.sortByColumn(0); + grid_.sortByColumn(1); + Assertions.assertIterableEquals( + List.of(view.third, view.first, view.second), + List.of(grid_.getRow(0), grid_.getRow(1), grid_.getRow(2))); + + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/Person.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/Person.java new file mode 100644 index 000000000..c43341acc --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/Person.java @@ -0,0 +1,191 @@ +/** + * Copyright (C) 2000-2026 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.grid; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * Copied from grid integration-tests. + */ +public class Person implements Serializable, Cloneable { + private int id; + private String firstName; + private String lastName; + private String email; + private int age; + private Address address; + private boolean deceased; + private Date birthDate; + private boolean isSubscriber; + + private Integer salary; // null if unknown + private Double salaryDouble; // null if unknown + + private BigDecimal rent; + + public Person() { + + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public String toString() { + return "Person [firstName=" + firstName + ", lastName=" + lastName + + ", email=" + email + ", age=" + age + ", deceased=" + deceased + + ", address=" + address + ", salary=" + salary + + ", salaryDouble=" + salaryDouble + ", rent=" + rent + "]"; + } + + public Person(String firstName, String lastName, String email, int age, + Address address) { + super(); + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.age = age; + this.address = address; + } + + public Person(String firstName, int age) { + super(); + this.firstName = firstName; + this.age = age; + } + + public boolean isSubscriber() { + return isSubscriber; + } + + public void setSubscriber(boolean isSubscriber) { + this.isSubscriber = isSubscriber; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean getDeceased() { + return deceased; + } + + public void setDeceased(boolean deceased) { + this.deceased = deceased; + } + + public Integer getSalary() { + return salary; + } + + public void setSalary(Integer salary) { + this.salary = salary; + } + + public BigDecimal getRent() { + return rent; + } + + public void setRent(BigDecimal rent) { + this.rent = rent; + } + + public Double getSalaryDouble() { + return salaryDouble; + } + + public void setSalaryDouble(Double salaryDouble) { + this.salaryDouble = salaryDouble; + } + + public Date getBirthDate() { + return birthDate; + } + + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate; + } + + @Override + public Person clone() { + try { + return (Person) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException("The Person object could not be cloned.", + e); + } + } + + public static Person createTestPerson1() { + return new Person("Jorma", "Järvi", "yeah@cool.com", 46, + new Address("Street", 1123, "Turku", Country.FINLAND)); + } + + public static Person createTestPerson2() { + return new Person("Maya", "Dinkelstein", "maya@foo.bar", 18, + new Address("Red street", 12, "Amsterdam", + Country.NETHERLANDS)); + } + + public static Person createTestPerson3() { + return new Person("Richard", "Johnson", "rt@bar.com", 30, + new Address("1st Avenue", 12, "Dallas", Country.USA)); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/RendererGridView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/RendererGridView.java new file mode 100644 index 000000000..42636e9c9 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/RendererGridView.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2000-2026 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.grid; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.HasText; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.data.renderer.ComponentRenderer; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "renderer-grid", registerAtStartup = false) +public class RendererGridView extends Component implements HasComponents { + + Grid grid = new Grid<>(); + Person first = Person.createTestPerson1(); + Person second = Person.createTestPerson2(); + + public RendererGridView() { + grid.setItems(first, second); + + grid.addColumn(new ComponentRenderer<>(p -> new Container( + new Text(p.getFirstName()), new TextNode(p.getLastName()), + new TextNode(Integer.toString(p.getAge()))))) + .setKey("componentRendered"); + grid.addColumn(new ComponentRenderer<>(p -> null)) + .setKey("nullRendered"); + grid.addColumn(new ComponentRenderer<>(p -> new Icon("USER"))); + add(grid); + } + + @Tag("a-container") + public static class Container extends Component implements HasComponents { + public Container(Component... components) { + add(components); + } + } + + @Tag("icon") + public static class Icon extends Component { + public Icon(String name) { + getElement().setProperty("name", name); + } + } + + @Tag("text-node") + public static class TextNode extends Component { + public TextNode(String text) { + super(Element.createText(text)); + } + } + + @Tag("text") + public static class Text extends Component implements HasText { + public Text(String text) { + setText(text); + } + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/SortGridView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/SortGridView.java new file mode 100644 index 000000000..0634226c9 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/SortGridView.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2000-2026 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.grid; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "sort-grid", registerAtStartup = false) +public class SortGridView extends Component implements HasComponents { + + Grid grid = new Grid<>(); + Grid beanGrid = new Grid<>(Person.class); + Person first = Person.createTestPerson1(); + Person second = Person.createTestPerson2(); + Person third = Person.createTestPerson3(); + + final String firstHeader = "Name"; + final String secondHeader = "Age"; + final String thirdHeader = "Email"; + + public SortGridView() { + + grid.setItems(first, second, third); + + grid.addColumn(Person::getFirstName).setHeader(firstHeader) + .setSortable(true); + grid.addColumn(Person::getAge).setHeader(secondHeader) + .setSortable(true); + grid.addColumn(Person::getEmail).setHeader(thirdHeader) + .setSortable(false); + + beanGrid.setItems(first, second, third); + beanGrid.getColumnByKey("firstName"); + beanGrid.getColumnByKey("age"); + beanGrid.getColumnByKey("email").setSortable(false); + + add(grid, beanGrid); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/AnchorTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/AnchorTesterTest.java new file mode 100644 index 000000000..6181d8a4a --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/AnchorTesterTest.java @@ -0,0 +1,174 @@ +/** + * Copyright (C) 2000-2026 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.html.tester; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.router.RouteConfiguration; +import com.vaadin.flow.server.StreamResource; +import com.vaadin.flow.server.streams.DownloadHandler; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ViewPackages +class AnchorTesterTest extends BrowserlessTest { + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(AnchorView.class); + } + + @Test + void anchorClick_navigatesCorrectly() { + Anchor anchor = new Anchor("anchor", "Home"); + UI.getCurrent().add(anchor); + + Assertions.assertEquals("anchor", test(anchor).getHref()); + Assertions.assertInstanceOf(AnchorView.class, test(anchor).navigate(), + "Click anchor did not navigate to AnchorView"); + } + + @Test + void anchorClick_navigatesCorrectlyWithParameters() { + Anchor anchor = new Anchor("anchor?name=value", "Home"); + UI.getCurrent().add(anchor); + + Assertions.assertEquals("anchor?name=value", test(anchor).getHref()); + Assertions.assertEquals("anchor", test(anchor).getPath()); + Assertions.assertEquals("name=value", + test(anchor).getQueryParameters().getQueryString()); + Assertions.assertInstanceOf(AnchorView.class, test(anchor).navigate(), + "Click anchor did not navigate to AnchorView"); + } + + @Test + void anchorClick_disabled_throws() { + Anchor anchor = new Anchor("anchor", "Home"); + anchor.setEnabled(false); + UI.getCurrent().add(anchor); + + assertThrows(IllegalStateException.class, () -> test(anchor).click()); + } + + @Test + void anchorClick_notARegisteredRoute_throws() { + Anchor anchor = new Anchor("error", "Home"); + UI.getCurrent().add(anchor); + + final IllegalStateException exception = assertThrows( + IllegalStateException.class, () -> test(anchor).click()); + + Assertions.assertEquals("Anchor is not for an application route", + exception.getMessage()); + } + + @Test + void anchorClick_streamRegistration_throws() { + StreamResource resource = new StreamResource("filename", + () -> new ByteArrayInputStream( + "Hello world".getBytes(StandardCharsets.UTF_8))); + Anchor anchor = new Anchor(resource, "Home"); + UI.getCurrent().add(anchor); + + final IllegalStateException exception = assertThrows( + IllegalStateException.class, () -> test(anchor).click()); + + Assertions.assertEquals("Anchor target seems to be a resource", + exception.getMessage()); + } + + @Test + void anchorDownload_writesResourceToOutputStream() { + StreamResource resource = new StreamResource("filename", + () -> new ByteArrayInputStream( + "Hello world".getBytes(StandardCharsets.UTF_8))); + + Anchor anchor = new Anchor(resource, "Download"); + UI.getCurrent().add(anchor); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + test(anchor).download(outputStream); + + Assertions.assertEquals("Hello world", + outputStream.toString(StandardCharsets.UTF_8)); + } + + @Test + void anchorDownload_disabled_throws() { + DownloadHandler resource = event -> event.getOutputStream() + .write("Hello world".getBytes(StandardCharsets.UTF_8)); + + Anchor anchor = new Anchor(resource, "Download"); + anchor.setEnabled(false); + UI.getCurrent().add(anchor); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + assertThrows(IllegalStateException.class, + () -> test(anchor).download(outputStream)); + } + + @Test + void anchorClick_downloadHandler_throws() { + DownloadHandler resource = event -> event.getOutputStream() + .write("Hello world".getBytes(StandardCharsets.UTF_8)); + Anchor anchor = new Anchor(resource, "Home"); + UI.getCurrent().add(anchor); + + final IllegalStateException exception = assertThrows( + IllegalStateException.class, () -> test(anchor).click()); + + Assertions.assertEquals("Anchor target seems to be a resource", + exception.getMessage()); + } + + @Test + void anchorDownload_downloadHandler_writesResourceToOutputStream() { + DownloadHandler resource = event -> event.getOutputStream() + .write("Hello world".getBytes(StandardCharsets.UTF_8)); + Anchor anchor = new Anchor(resource, "Download"); + UI.getCurrent().add(anchor); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + test(anchor).download(outputStream); + + Assertions.assertEquals("Hello world", + outputStream.toString(StandardCharsets.UTF_8)); + } + + @Test + void anchorDownload_noStreamRegistration_throws() { + Anchor anchor = new Anchor("anchor", "Download"); + UI.getCurrent().add(anchor); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> test(anchor).download(outputStream)); + + Assertions.assertEquals("Anchor target does not seem to be a resource", + exception.getMessage()); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/AnchorView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/AnchorView.java new file mode 100644 index 000000000..1faf25fbb --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/AnchorView.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2000-2026 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.html.tester; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("a") +@Route(value = "anchor", registerAtStartup = false) +public class AnchorView extends Component { + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/RangeInputTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/RangeInputTesterTest.java new file mode 100644 index 000000000..e93777594 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/RangeInputTesterTest.java @@ -0,0 +1,212 @@ +/** + * Copyright (C) 2000-2026 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.html.tester; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.component.html.RangeInput; +import com.vaadin.flow.component.html.RangeInputTester; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class RangeInputTesterTest extends BrowserlessTest { + + RangeInputTester tester; + RangeInput component; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(RangeInputView.class); + component = navigate(RangeInputView.class).rangeInput; + tester = test(component); + } + + @Test + void setValue_unusable_throws() { + component.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tester.setValue(12.3)); + + component.setEnabled(true); + component.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tester.setValue(12.3)); + + } + + @Test + void setValue_allowedValue_valueSet() { + double newValue = 12.0; + tester.setValue(newValue); + Assertions.assertEquals(newValue, component.getValue()); + + newValue = component.getMin(); + tester.setValue(newValue); + Assertions.assertEquals(newValue, component.getValue()); + + newValue = component.getMax(); + tester.setValue(newValue); + Assertions.assertEquals(newValue, component.getValue()); + } + + @Test + void setValue_null_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tester.setValue(null)); + } + + @Test + void setValue_valueNotRespectingStep_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tester.setValue(12.3), + "Accepted value not respecting step"); + component.setValue(4.0); + Assertions.assertEquals(4, 0, component.getValue()); + component.setValue(3.0); + Assertions.assertEquals(3.0, component.getValue()); + } + + @Test + void setValue_customStep() { + double step = 3.3; + double initialValue = 1.3; + component.setStep(step); + component.setValue(initialValue); + + double newValue = initialValue + (step * 2); + tester.setValue(newValue); + Assertions.assertEquals(newValue, component.getValue()); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> tester.setValue(newValue + 1), + "Accepted value not matching step"); + } + + @Test + void setValue_undefinedStep_valueAccepted() { + component.setStep(null); + double newValue = 12.3; + tester.setValue(newValue); + Assertions.assertEquals(newValue, component.getValue()); + } + + @Test + void setValue_lessThanMin_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tester.setValue(-100.0), "Accepted value less than min"); + } + + @Test + void setValue_greaterThanMax_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tester.setValue(100.0), + "Accepted value greater than max"); + } + + @Test + void getValue_unusable_getsValue() { + double newValue = 12.4; + component.setValue(newValue); + Assertions.assertEquals(newValue, component.getValue()); + } + + @Test + void getValue_hidden_throws() { + component.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tester.getValue()); + } + + @Test + void increase_addStepToValue() { + tester.increase(); + Assertions.assertEquals(1.0, component.getValue()); + tester.increase(); + Assertions.assertEquals(2.0, component.getValue()); + tester.increase(5); + Assertions.assertEquals(7.0, component.getValue()); + + } + + @Test + void increase_negativeTimes_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tester.increase(-1)); + } + + @Test + void increase_greaterThanMax_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tester.increase(30)); + } + + @Test + void increase_undefinedStep_throws() { + component.setStep(null); + Assertions.assertThrows(IllegalStateException.class, + () -> tester.increase()); + } + + @Test + void increase_unusable_throws() { + component.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tester.increase()); + } + + @Test + void decrease_addStepToValue() { + tester.decrease(); + Assertions.assertEquals(-1.0, component.getValue()); + tester.decrease(); + Assertions.assertEquals(-2.0, component.getValue()); + tester.decrease(5); + Assertions.assertEquals(-7.0, component.getValue()); + + } + + @Test + void decrease_lessThanMin_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tester.decrease(30)); + } + + @Test + void decrease_undefinedStep_throws() { + component.setStep(null); + Assertions.assertThrows(IllegalStateException.class, + () -> tester.decrease()); + } + + @Test + void decrease_unusable_throws() { + component.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tester.decrease()); + } + + @Test + void decrease_negativeTimes_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tester.decrease(-1)); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/RangeInputView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/RangeInputView.java new file mode 100644 index 000000000..23af68eee --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/html/tester/RangeInputView.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2000-2026 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.html.tester; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.html.RangeInput; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "range-input", registerAtStartup = false) +public class RangeInputView extends Component implements HasComponents { + + RangeInput rangeInput = new RangeInput(); + + public RangeInputView() { + rangeInput.setMin(-20); + rangeInput.setMax(20); + add(rangeInput); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/listbox/ListBoxTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/listbox/ListBoxTesterTest.java new file mode 100644 index 000000000..b10620e6f --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/listbox/ListBoxTesterTest.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2000-2026 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.listbox; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +import static org.junit.jupiter.api.Assertions.assertIterableEquals; + +@ViewPackages +class ListBoxTesterTest extends BrowserlessTest { + ListBoxView view; + + @BeforeEach + public void registerView() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(ListBoxView.class); + view = navigate(ListBoxView.class); + } + + @Test + void getSuggestionItems_returnsAllItems() { + assertIterableEquals(view.selection, + test(view.listBox).getSuggestionItems()); + } + + @Test + void stringSelect_getSuggestions_valuesEqualItems() { + assertIterableEquals(view.selection, + test(view.listBox).getSuggestions()); + } + + @Test + void stringSelect_selectItem_selectsCorrectItem() { + Assertions.assertNull(test(view.listBox).getSelected()); + + test(view.listBox).selectItem("two"); + + Assertions.assertSame(view.selection.get(1), + test(view.listBox).getSelected()); + + test(view.listBox).selectItem(null); + + Assertions.assertNull(test(view.listBox).getSelected(), + "Selecting null should clear selection"); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/listbox/ListBoxView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/listbox/ListBoxView.java new file mode 100644 index 000000000..2ead0bd35 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/listbox/ListBoxView.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2000-2026 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.listbox; + +import java.util.Arrays; +import java.util.List; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "list-boxes", registerAtStartup = false) +public class ListBoxView extends Component implements HasComponents { + ListBox listBox; + MultiSelectListBox multiSelectListBox; + + List selection = Arrays.asList("one", "two"); + + public ListBoxView() { + listBox = new ListBox<>(); + listBox.setItems(selection); + + multiSelectListBox = new MultiSelectListBox<>(); + multiSelectListBox.setItems(selection); + + add(listBox, multiSelectListBox); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/listbox/MultiSelectListBoxTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/listbox/MultiSelectListBoxTesterTest.java new file mode 100644 index 000000000..02f1e5c69 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/listbox/MultiSelectListBoxTesterTest.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2000-2026 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.listbox; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +import static org.junit.jupiter.api.Assertions.assertIterableEquals; + +@ViewPackages +class MultiSelectListBoxTesterTest extends BrowserlessTest { + ListBoxView view; + + @BeforeEach + public void registerView() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(ListBoxView.class); + view = navigate(ListBoxView.class); + } + + @Test + void getSuggestionItems_returnsAllItems() { + assertIterableEquals(view.selection, + test(view.multiSelectListBox).getSuggestionItems()); + } + + @Test + void stringSelect_getSuggestions_valuesEqualItems() { + assertIterableEquals(view.selection, + test(view.multiSelectListBox).getSuggestions()); + } + + @Test + void stringSelect_selectItemsDeselectItems_selectsCorrectItem() { + final MultiSelectListBoxTester, String> list_ = test( + view.multiSelectListBox); + Assertions.assertTrue(list_.getSelected().isEmpty()); + + list_.selectItems("two"); + + Assertions.assertIterableEquals(view.selection.subList(1, 2), + list_.getSelected()); + + list_.deselectItems("two"); + + Assertions.assertTrue(list_.getSelected().isEmpty(), + "Deselecting item should clear selection"); + } + + @Test + void stringSelect_selectItems_addsToSelection() { + final MultiSelectListBoxTester, String> list_ = test( + view.multiSelectListBox); + Assertions.assertTrue(list_.getSelected().isEmpty()); + + list_.selectItems("two"); + + Assertions.assertIterableEquals(view.selection.subList(1, 2), + list_.getSelected()); + + list_.selectItems("one"); + + Assertions.assertIterableEquals(view.selection, list_.getSelected()); + } + + @Test + void clearSelection_selectItems_addsToSelection() { + final MultiSelectListBoxTester, String> list_ = test( + view.multiSelectListBox); + Assertions.assertTrue(list_.getSelected().isEmpty()); + + list_.selectItems("two", "one"); + + Assertions.assertIterableEquals(view.selection, list_.getSelected()); + + list_.clearSelection(); + + Assertions.assertTrue(list_.getSelected().isEmpty()); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginFormTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginFormTesterTest.java new file mode 100644 index 000000000..4751e7313 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginFormTesterTest.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2000-2026 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.login; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +public class LoginFormTesterTest extends BrowserlessTest { + + LoginFormView view; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(LoginFormView.class); + view = navigate(LoginFormView.class); + } + + @Test + void login_generatesLoginEvent() { + test(view.login).login("user", "pwd"); + Assertions.assertEquals(1, $(Span.class).from(view).all().size()); + Span message = $(Span.class).from(view).withId("m1").first(); + Assertions.assertEquals(view.generateLoginMessage("user", "pwd"), + message.getText()); + } + + @Test + void login_disablesLoginComponent() { + test(view.login).login("admin", "adm"); + + Assertions.assertFalse(test(view.login).isUsable(), + "Login should be disabled after a login event."); + + Assertions.assertThrows(IllegalStateException.class, + () -> test(view.login).login("us", "er"), + "Disabled login should not accept login event"); + } + + @Test + void forgotPassword_generatesEvent() { + test(view.login).forgotPassword(); + + Assertions.assertEquals(1, $(Span.class).from(view).all().size()); + Span message = $(Span.class).from(view).withId("m1").first(); + Assertions.assertEquals("forgot", message.getText()); + } + + @Test + void forgotButtonHidden_usingForgotThrows() { + view.login.setForgotPasswordButtonVisible(false); + + Assertions.assertThrows(IllegalStateException.class, + () -> test(view.login).forgotPassword(), + "Hidden forgot password button should not be usable."); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginFormView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginFormView.java new file mode 100644 index 000000000..02dabe947 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginFormView.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2000-2026 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.login; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "login-view", registerAtStartup = false) +public class LoginFormView extends Component implements HasComponents { + LoginForm login; + Div messages; + + public LoginFormView() { + this.login = new LoginForm(); + messages = new Div(new Text("Messages")); + login.addLoginListener(loginEvent -> addMessage(generateLoginMessage( + loginEvent.getUsername(), loginEvent.getPassword()))); + login.addForgotPasswordListener( + forgotPasswordEvent -> addMessage("forgot")); + add(login, messages); + } + + protected static String generateLoginMessage(String user, String password) { + return "login: " + user + " :: " + password; + } + + protected void addMessage(String message) { + Span messageElement = new Span(message); + messageElement.setId("m" + messages.getChildren().count()); + messages.add(messageElement); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginOverlayTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginOverlayTesterTest.java new file mode 100644 index 000000000..a542d88cf --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginOverlayTesterTest.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2000-2026 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.login; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +public class LoginOverlayTesterTest extends BrowserlessTest { + + LoginOverlayView view; + LoginOverlayTester login_; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(LoginOverlayView.class); + view = navigate(LoginOverlayView.class); + login_ = test(view.login); + } + + @Test + void overlayClosed_throwsException() { + Assertions.assertThrows(IllegalStateException.class, + () -> login_.login("user", "user")); + } + + @Test + void login_generatesLoginEvent() { + login_.openOverlay(); + + login_.login("user", "pwd"); + Assertions.assertEquals(1, $(Span.class).from(view).all().size()); + Span message = $(Span.class).from(view).withId("m1").first(); + Assertions.assertEquals(view.generateLoginMessage("user", "pwd"), + message.getText()); + } + + @Test + void login_disablesLoginComponent() { + login_.openOverlay(); + login_.login("admin", "adm"); + + Assertions.assertFalse(login_.isUsable(), + "Login should be disabled after a login event."); + + Assertions.assertThrows(IllegalStateException.class, + () -> login_.login("us", "er"), + "Disabled login should not accept login event"); + } + + @Test + void forgotPassword_generatesEvent() { + login_.openOverlay(); + login_.forgotPassword(); + + Assertions.assertEquals(1, $(Span.class).from(view).all().size()); + Span message = $(Span.class).from(view).withId("m1").first(); + Assertions.assertEquals("forgot", message.getText()); + } + + @Test + void forgotButtonHidden_usingForgotThrows() { + login_.openOverlay(); + view.login.setForgotPasswordButtonVisible(false); + + Assertions.assertThrows(IllegalStateException.class, + () -> login_.forgotPassword(), + "Hidden forgot password button should not be usable."); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginOverlayView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginOverlayView.java new file mode 100644 index 000000000..5de3eaf28 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/login/LoginOverlayView.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2000-2026 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.login; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "login-view", registerAtStartup = false) +public class LoginOverlayView extends Component implements HasComponents { + LoginOverlay login; + Div messages; + + public LoginOverlayView() { + this.login = new LoginOverlay(); + messages = new Div(new Text("Messages")); + login.addLoginListener(loginEvent -> addMessage(generateLoginMessage( + loginEvent.getUsername(), loginEvent.getPassword()))); + login.addForgotPasswordListener( + forgotPasswordEvent -> addMessage("forgot")); + + Button open = new Button("Login", event -> login.setOpened(true)); + add(open, login, messages); + } + + protected static String generateLoginMessage(String user, String password) { + return "login: " + user + " :: " + password; + } + + protected void addMessage(String message) { + Span messageElement = new Span(message); + messageElement.setId("m" + messages.getChildren().count()); + messages.add(messageElement); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarTesterTest.java new file mode 100644 index 000000000..ed9d040c9 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarTesterTest.java @@ -0,0 +1,267 @@ +/** + * Copyright (C) 2000-2026 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.menubar; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class MenuBarTesterTest extends BrowserlessTest { + + MenuBarView view; + MenuBarTester menu_; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(MenuBarView.class); + view = navigate(MenuBarView.class); + menu_ = test(view.menu); + } + + @Test + void clickItem_byText_actionExecuted() { + menu_.clickItem("Foo"); + Assertions.assertIterableEquals(List.of("Foo"), view.clickedItems); + + menu_.clickItem("Text"); + menu_.clickItem("Bar"); + Assertions.assertIterableEquals(List.of("Foo", "Text", "Bar"), + view.clickedItems); + } + + @Test + void clickItem_notExisting_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.clickItem("XYZ")); + } + + @Test + void clickItem_menuNotUsable_throws() { + view.menu.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + () -> menu_.clickItem("Bar")); + + view.menu.setEnabled(true); + view.menu.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, + () -> menu_.clickItem("Bar")); + } + + @Test + void clickItem_multipleMatches_throws() { + Assertions.assertThrows(IllegalStateException.class, + () -> menu_.clickItem("Duplicated")); + } + + @Test + void clickItem_checkable_checkStatusChanges() { + menu_.clickItem("Checkables", "Checkable"); + Assertions.assertIterableEquals(List.of("Checkable"), + view.clickedItems); + Assertions.assertTrue(view.checkableItem.isChecked(), + "Item should be checked but was not"); + + menu_.clickItem("Checkables", "Checkable"); + Assertions.assertIterableEquals(List.of("Checkable", "Checkable"), + view.clickedItems); + Assertions.assertFalse(view.checkableItem.isChecked(), + "Item should not be checked but was"); + } + + @Test + void clickItem_disabled_throws() { + Assertions.assertThrows(IllegalStateException.class, + () -> menu_.clickItem("Disabled")); + Assertions.assertTrue(view.clickedItems.isEmpty(), + "Listener should not have been notified"); + } + + @Test + void clickItem_hidden_throws() { + Assertions.assertThrows(IllegalStateException.class, + () -> menu_.clickItem("Hidden")); + Assertions.assertTrue(view.clickedItems.isEmpty(), + "Listener should not have been notified"); + } + + @Test + void clickItem_nested_executeAction() { + menu_.clickItem("Hierarchical", "Level2"); + Assertions.assertIterableEquals(List.of("Hierarchical / Level2"), + view.clickedItems); + + view.clickedItems.clear(); + menu_.clickItem("Hierarchical", "NestedSubMenu", "Level3"); + Assertions.assertIterableEquals( + List.of("Hierarchical / NestedSubMenu / Level3"), + view.clickedItems); + } + + @Test + void clickItem_nestedWrongPath_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.clickItem("Foo", "Bar")); + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.clickItem("Hierarchical", "Level3")); + Assertions.assertThrows(IllegalArgumentException.class, () -> menu_ + .clickItem("Hierarchical", "NestedSubMenu", "Level3Bis")); + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.clickItem("Hierarchical", "NestedSubMenu", "Level3", + "Level4")); + } + + @Test + void clickItem_nestedNotUsableParent_throws() { + Assertions.assertThrows(IllegalStateException.class, () -> menu_ + .clickItem("Hierarchical", "NestedDisabled", "Level3")); + Assertions.assertThrows(IllegalStateException.class, () -> menu_ + .clickItem("Hierarchical", "NestedInvisible", "Level3")); + + } + + @Test + void clickItem_byIndex_executesAction() { + menu_.clickItem(0); + Assertions.assertIterableEquals(List.of("Foo"), view.clickedItems); + + menu_.clickItem(2); + menu_.clickItem(1); + Assertions.assertIterableEquals(List.of("Foo", "Text", "Bar"), + view.clickedItems); + + } + + @Test + void clickItem_byInvalidIndexes_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.clickItem(1, 2)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.clickItem(7, 5)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.clickItem(7, 1, 5)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.clickItem(7, 1, 0, 4)); + } + + @Test + void clickItem_byNestedIndexNotUsableParent_throws() { + // Hierarchical / NestedDisabled / Level3 + Assertions.assertThrows(IllegalStateException.class, + () -> menu_.clickItem(7, 3, 0)); + } + + @Test + void clickItem_byNestedIndexes_executesAction() { + menu_.clickItem(7, 0); + Assertions.assertIterableEquals(List.of("Hierarchical / Level2"), + view.clickedItems); + + view.clickedItems.clear(); + menu_.clickItem(7, 1, 0); + Assertions.assertIterableEquals( + List.of("Hierarchical / NestedSubMenu / Level3"), + view.clickedItems); + + } + + @Test + void isItemChecked_byText_getCheckedStatus() { + Assertions.assertFalse(menu_.isItemChecked("Checkables", "Checkable"), + "Checkable item should not be checked by default, but result is true"); + + view.checkableItem.setChecked(true); + Assertions.assertTrue(menu_.isItemChecked("Checkables", "Checkable"), + "Checkable item is checked, but result is false"); + + view.checkableItem.setChecked(false); + Assertions.assertFalse(menu_.isItemChecked("Checkables", "Checkable"), + "Checkable item is not checked, but result is true"); + } + + @Test + void isItemChecked_byIndex_getCheckedStatus() { + Assertions.assertFalse(menu_.isItemChecked(5, 0), + "Checkable item should not be checked by default, but result is true"); + + view.checkableItem.setChecked(true); + Assertions.assertTrue(menu_.isItemChecked(5, 0), + "Checkable item is checked, but result is false"); + + view.checkableItem.setChecked(false); + Assertions.assertFalse(menu_.isItemChecked(5, 0), + "Checkable item is not checked, but result is true"); + } + + @Test + void isItemChecked_nestedByText_getCheckedStatus() { + Assertions.assertTrue( + menu_.isItemChecked("Hierarchical", "Nested Checkable"), + "Checkable item should be checked by default, but result is false"); + + view.nestedCheckableItem.setChecked(false); + Assertions.assertFalse( + menu_.isItemChecked("Hierarchical", "Nested Checkable"), + "Checkable item is not checked, but result is true"); + + view.nestedCheckableItem.setChecked(true); + Assertions.assertTrue( + menu_.isItemChecked("Hierarchical", "Nested Checkable"), + "Checkable item is checked, but result is false"); + } + + @Test + void isItemChecked_nestedByIndex_getCheckedStatus() { + Assertions.assertTrue(menu_.isItemChecked(7, 2), + "Checkable item should be checked by default, but result is false"); + + view.nestedCheckableItem.setChecked(false); + Assertions.assertFalse(menu_.isItemChecked(7, 2), + "Checkable item is not checked, but result is true"); + + view.nestedCheckableItem.setChecked(true); + Assertions.assertTrue(menu_.isItemChecked(7, 2), + "Checkable item is checked, but result is false"); + } + + @Test + void isItemChecked_notCheckableItem_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.isItemChecked("Bar")); + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.isItemChecked("Hierarchical", "Level2")); + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.isItemChecked(0)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.isItemChecked(7, 1)); + } + + @Test + void isItemChecked_notExisting_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.isItemChecked("XYZ")); + Assertions.assertThrows(IllegalArgumentException.class, + () -> menu_.isItemChecked(22)); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarView.java new file mode 100644 index 000000000..61ae7022d --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarView.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2000-2026 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.menubar; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.contextmenu.SubMenu; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "contextmenu", registerAtStartup = false) +public class MenuBarView extends Component implements HasComponents { + + final MenuItem checkableItem; + final MenuItem nestedCheckableItem; + final List clickedItems = new ArrayList<>(); + final MenuBar menu; + + public MenuBarView() { + Span assignee = new Span(); + menu = new MenuBar(); + menu.addItem("Foo", ev -> clickedItems.add("Foo")); + menu.addItem("Bar", ev -> clickedItems.add("Bar")); + menu.addItem("Text", ev -> clickedItems.add("Text")); + menu.addItem("Duplicated", ev -> clickedItems.add("Duplicated 1")); + menu.addItem("Duplicated", ev -> clickedItems.add("Duplicated 2")); + SubMenu checkablesSubMenu = menu.addItem("Checkables").getSubMenu(); + checkableItem = checkablesSubMenu.addItem("Checkable", + ev -> clickedItems.add("Checkable")); + checkableItem.setCheckable(true); + menu.addItem("Disabled", ev -> clickedItems.add("Disabled")) + .setEnabled(false); + menu.addItem("Hidden", ev -> clickedItems.add("Hidden")) + .setVisible(false); + + SubMenu subMenu = menu.addItem("Hierarchical").getSubMenu(); + subMenu.addItem("Level2", + ev -> clickedItems.add("Hierarchical / Level2")); + subMenu.addItem("NestedSubMenu").getSubMenu().addItem("Level3", + ev -> clickedItems + .add("Hierarchical / NestedSubMenu / Level3")); + nestedCheckableItem = subMenu.addItem("Nested Checkable"); + nestedCheckableItem.setCheckable(true); + nestedCheckableItem.setChecked(true); + MenuItem nestedDisabled = subMenu.addItem("NestedDisabled"); + nestedDisabled.setEnabled(false); + nestedDisabled.getSubMenu().addItem("Level3", ev -> clickedItems + .add("Hierarchical / NestedDisabled / Level3")); + MenuItem nestedInvisible = subMenu.addItem("NestedInvisible"); + nestedInvisible.setVisible(false); + nestedInvisible.getSubMenu().addItem("Level3", ev -> clickedItems + .add("Hierarchical / NestedInvisible / Level3")); + + add(menu); + } + + @Tag("span") + private static class Span extends Component { + + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/messages/MessageInputTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/messages/MessageInputTesterTest.java new file mode 100644 index 000000000..6bedac402 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/messages/MessageInputTesterTest.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2000-2026 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.messages; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class MessageInputTesterTest extends BrowserlessTest { + + MessagesView view; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(MessagesView.class); + view = navigate(MessagesView.class); + } + + @Test + void emptyMessage_noEventIsFired() { + AtomicReference message = new AtomicReference<>(); + + view.input.addSubmitListener(submitEvent -> message.compareAndSet(null, + submitEvent.getValue())); + + final String testMessage = ""; + test(view.input).send(testMessage); + Assertions.assertNull(message.get()); + } + + @Test + void sendMessage_eventIsReceived() { + AtomicReference message = new AtomicReference<>(); + + view.input.addSubmitListener(submitEvent -> message.compareAndSet(null, + submitEvent.getValue())); + + final String testMessage = "Hello"; + test(view.input).send(testMessage); + Assertions.assertEquals(testMessage, message.get()); + } + + @Test + void disabledMessage_throwsException() { + AtomicReference message = new AtomicReference<>(); + + view.input.addSubmitListener(submitEvent -> message.compareAndSet(null, + submitEvent.getValue())); + view.input.setEnabled(false); + + final String testMessage = "Hello"; + Assertions.assertThrows(IllegalStateException.class, + () -> test(view.input).send(testMessage)); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/messages/MessageListTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/messages/MessageListTesterTest.java new file mode 100644 index 000000000..fce5964ee --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/messages/MessageListTesterTest.java @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2000-2026 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.messages; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.TreeOnFailureExtension; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +@ExtendWith(TreeOnFailureExtension.class) +class MessageListTesterTest extends BrowserlessTest { + + MessagesView view; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(MessagesView.class); + view = navigate(MessagesView.class); + } + + @Test + void size_returnsCorrectSize() { + Assertions.assertEquals(3, test(view.list).size()); + + addItem(view.list, new MessageListItem("Added message")); + + Assertions.assertEquals(4, test(view.list).size(), + "Message should have been added to the list"); + } + + @Test + void getMessages_allMessagesReturned() { + Assertions.assertIterableEquals( + Arrays.asList(view.one, view.two, view.three), + test(view.list).getMessages()); + } + + @Test + void getMessageByIndex_correctMessageReturned() { + final MessageListTester list_ = test(view.list); + Assertions.assertEquals(view.two, list_.getMessage(1)); + Assertions.assertEquals(view.three, list_.getMessage(2)); + Assertions.assertEquals(view.one, list_.getMessage(0)); + } + + @Test + void getMessageFromTime_messagesAfterAreReturned() { + final MessageListTester list_ = test(view.list); + Assertions.assertIterableEquals(Arrays.asList(view.two, view.three), + list_.getMessagesAfter(LocalDateTime.now().withHour(11) + .toInstant(ZoneOffset.UTC))); + Assertions.assertIterableEquals(Arrays.asList(view.three), + list_.getMessagesAfter(LocalDateTime.now().withHour(12) + .toInstant(ZoneOffset.UTC))); + } + + @Test + void getMessageUpToTime_messagesBeforeAreReturned() { + final MessageListTester list_ = test(view.list); + Assertions.assertIterableEquals(Arrays.asList(view.one, view.two), + list_.getMessagesBefore(LocalDateTime.now().withHour(13) + .toInstant(ZoneOffset.UTC))); + Assertions.assertIterableEquals(Arrays.asList(view.one), + list_.getMessagesBefore(LocalDateTime.now().withHour(11) + .toInstant(ZoneOffset.UTC))); + } + + @Test + void getMessageBetweenTime_matchingMessagesReturned() { + Assertions.assertIterableEquals(Arrays.asList(view.two), + test(view.list).getMessages( + LocalDateTime.now().withHour(1) + .toInstant(ZoneOffset.UTC), + LocalDateTime.now().withHour(13) + .toInstant(ZoneOffset.UTC))); + } + + @Test + void getMessageForUser_messagesForUserReturned() { + final MessageListTester list_ = test(view.list); + Assertions.assertIterableEquals(Arrays.asList(view.one, view.three), + list_.getMessages("Joe")); + Assertions.assertIterableEquals(Arrays.asList(view.two), + list_.getMessages("Jane")); + + Assertions.assertTrue(list_.getMessages(null).isEmpty()); + + final MessageListItem nullUser = new MessageListItem("hi"); + addItem(view.list, nullUser); + + Assertions.assertIterableEquals(Arrays.asList(nullUser), + list_.getMessages(null)); + } + + /** + * Add a new message item to the MessageList. + * + * @param item + * item to add + */ + private void addItem(MessageList list, MessageListItem item) { + + final ArrayList messageListItems = new ArrayList<>( + list.getItems()); + messageListItems.add(item); + list.setItems(messageListItems); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/messages/MessagesView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/messages/MessagesView.java new file mode 100644 index 000000000..e53fbc96a --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/messages/MessagesView.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2000-2026 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.messages; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "messages", registerAtStartup = false) +public class MessagesView extends Component implements HasComponents { + + MessageInput input; + MessageList list; + + MessageListItem one = new MessageListItem("one", + LocalDateTime.now().withHour(0).toInstant(ZoneOffset.UTC), "Joe"); + MessageListItem two = new MessageListItem("two", + LocalDateTime.now().withHour(12).toInstant(ZoneOffset.UTC), "Jane"); + MessageListItem three = new MessageListItem("three", + LocalDateTime.now().withHour(23).toInstant(ZoneOffset.UTC), "Joe"); + + public MessagesView() { + list = new MessageList(); + input = new MessageInput(); + list.setItems(one, two, three); + + add(input, list); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/notification/NotificationTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/notification/NotificationTesterTest.java new file mode 100644 index 000000000..eab1c6d4a --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/notification/NotificationTesterTest.java @@ -0,0 +1,189 @@ +/** + * Copyright (C) 2000-2026 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.notification; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class NotificationTesterTest extends BrowserlessTest { + + @BeforeEach + public void registerView() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(NotificationView.class); + navigate(NotificationView.class); + } + + @Test + void showAndAutoClose_attachedAndDetached() { + Notification notification = Notification.show("Some text"); + roundTrip(); + + Assertions.assertTrue(notification.isAttached(), + "Notification should be attached after show"); + + NotificationTester notification_ = test(NotificationTester.class, + notification); + notification_.autoClose(); + + Assertions.assertFalse(notification.isAttached(), + "Notification should not be attached after auto-close"); + } + + @Test + void programmaticallyClose_notificationIsDetached() { + Notification notification = Notification.show("Some text"); + roundTrip(); + + notification.close(); + + Assertions.assertFalse(notification.isAttached(), + "Notification should not be attached after close"); + } + + @Test + void notOpenedNotification_isNotUsable() { + Notification notification = new Notification("Not Opened"); + + Assertions.assertFalse(test(notification).isUsable(), + "Not opened Notification shouldn't be usable"); + } + + @Test + void openedNotification_isUsable() { + String notificationText = "Opened notification"; + Notification notification = Notification.show(notificationText); + roundTrip(); + + Assertions.assertTrue(test(notification).isUsable(), + "Opened Notification should be usable"); + } + + @Test + void disabledNotification_isUsable() { + String notificationText = "Opened disabled notification"; + Notification notification = Notification.show(notificationText); + notification.setEnabled(false); + roundTrip(); + + Assertions.assertTrue(test(notification).isUsable(), + "Disabled Notification should be usable"); + } + + @Test + void hiddenNotification_isNotUsable() { + String notificationText = "Opened hidden notification"; + Notification notification = Notification.show(notificationText); + notification.setVisible(false); + roundTrip(); + + Assertions.assertFalse(test(notification).isUsable(), + "Hidden Notification should not be usable"); + } + + @Test + void closedNotification_notFlushed_isNotUsable() { + String notificationText = "Opened notification"; + Notification notification = Notification.show(notificationText); + roundTrip(); + notification.close(); + + Assertions.assertFalse(test(notification).isUsable(), + "Closed Notification should not be usable"); + } + + @Test + void autoClose_displayedNotification_isNotUsable() { + Notification notification = Notification.show("Some text"); + roundTrip(); + + NotificationTester notification_ = test(NotificationTester.class, + notification); + notification_.autoClose(); + + Assertions.assertFalse(notification_.isUsable(), + "Notification should not be usable after auto-close"); + } + + @Test + void autoClose_notOpenedNotification_throws() { + Notification notification = new Notification("Some text"); + + Assertions.assertThrows(IllegalStateException.class, + test(notification)::autoClose, + "Auto-close not opened notification should fail"); + } + + @Test + void autoClose_closedNotification_throws() { + Notification notification = Notification.show("Some text"); + roundTrip(); + + NotificationTester notification_ = test(NotificationTester.class, + notification); + notification_.autoClose(); + Assertions.assertThrows(IllegalStateException.class, + notification_::autoClose, + "Auto-close already closed notification should fail"); + } + + @Test + void autoClose_notificationWithDisabledAutoClose_throws() { + Notification notification = Notification.show("Some text"); + notification.setDuration(0); + roundTrip(); + + Assertions.assertThrows(IllegalStateException.class, + test(notification)::autoClose, + "Auto-close notification with auto-close disabled should fail"); + } + + @Test + void getText_displayedNotification_textContentAvailable() { + String notificationText = "Opened notification"; + Notification notification = Notification.show(notificationText); + roundTrip(); + + Assertions.assertEquals(notificationText, test(notification).getText()); + } + + @Test + void getText_notOpenedNotification_throws() { + Notification notification = new Notification("Some text"); + + Assertions.assertThrows(IllegalStateException.class, + test(notification)::getText, + "Getting text from not opened notification should fail"); + } + + @Test + void getText_closedNotification_throws() { + Notification notification = Notification.show("Some text"); + notification.close(); + roundTrip(); + + Assertions.assertThrows(IllegalStateException.class, + test(notification)::getText, + "Getting text from closed notification should fail"); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/notification/NotificationView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/notification/NotificationView.java new file mode 100644 index 000000000..2b31d2934 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/notification/NotificationView.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2000-2026 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.notification; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "notification", registerAtStartup = false) +public class NotificationView extends Component { + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/radiobutton/RadioButtonGroupTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/radiobutton/RadioButtonGroupTesterTest.java new file mode 100644 index 000000000..993ec974e --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/radiobutton/RadioButtonGroupTesterTest.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2000-2026 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.radiobutton; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.flow.router.RouteConfiguration; + +class RadioButtonGroupTesterTest extends BrowserlessTest { + + RadioButtonView view; + RadioButtonGroupTester, RadioButtonView.Name> buttonGroup_; + + @BeforeEach + public void registerView() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(RadioButtonView.class); + view = navigate(RadioButtonView.class); + buttonGroup_ = test(view.radioButtonGroup); + } + + @Test + void selectItem_selectCorrectItem() { + buttonGroup_.selectItem("test-bar"); + Assertions.assertEquals(view.items.get(1), buttonGroup_.getSelected()); + + buttonGroup_.selectItem("test-jay"); + Assertions.assertEquals(view.items.get(3), buttonGroup_.getSelected()); + } + + @Test + void deselectAll_noItemsSelected() { + view.radioButtonGroup.setValue(view.items.get(0)); + + buttonGroup_.deselectItem(); + RadioButtonView.Name selectedItem = buttonGroup_.getSelected(); + Assertions.assertNull(selectedItem, + "Expecting no selection, but got " + selectedItem); + } + + @Test + void selectItem_notExisting_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> buttonGroup_.selectItem("jay")); + } + + @Test + void selectItem_itemDisabled_throws() { + view.radioButtonGroup + .setItemEnabledProvider(n -> n.getName().startsWith("b")); + + // Items enabled, should work + buttonGroup_.selectItem("test-bar"); + buttonGroup_.selectItem("test-baz"); + + Assertions.assertThrows(IllegalStateException.class, + () -> buttonGroup_.selectItem("test-foo")); + Assertions.assertThrows(IllegalStateException.class, + () -> buttonGroup_.selectItem("test-jay")); + } + + @Test + void readOnly_isNotUsable() { + view.radioButtonGroup.setReadOnly(true); + + Assertions.assertThrows(IllegalStateException.class, + () -> buttonGroup_.selectItem("test-foo")); + Assertions.assertThrows(IllegalStateException.class, + () -> buttonGroup_.deselectItem()); + + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/radiobutton/RadioButtonTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/radiobutton/RadioButtonTesterTest.java new file mode 100644 index 000000000..65cbe99df --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/radiobutton/RadioButtonTesterTest.java @@ -0,0 +1,88 @@ +/** + * Copyright (C) 2000-2026 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.radiobutton; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class RadioButtonTesterTest extends BrowserlessTest { + + RadioButtonView view; + RadioButtonTester, String> radioButton_; + + @BeforeEach + public void registerView() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(RadioButtonView.class); + view = navigate(RadioButtonView.class); + radioButton_ = test(view.radioButton); + } + + @Test + void click_usable_valueChanges() { + Assertions.assertFalse(view.radioButton.isCheckedBoolean(), + "Expecting radioButton initial state not to be checked"); + + radioButton_.click(); + Assertions.assertTrue(view.radioButton.isCheckedBoolean(), + "Expecting radioButton to be checked, but was not"); + } + + @Test + void click_usable_checkedChangeFired() { + AtomicBoolean checkedChange = new AtomicBoolean(); + view.radioButton.getElement().addPropertyChangeListener("checked", + (ev -> checkedChange.set(true))); + Assertions.assertFalse(view.radioButton.isCheckedBoolean(), + "Expecting radioButton not to be checked, but was"); + + radioButton_.click(); + Assertions.assertTrue(checkedChange.get(), + "Expected radioButton change event to be fired, but was not"); + Assertions.assertTrue(view.radioButton.isCheckedBoolean(), + "Expecting radioButton not to be checked, but was"); + } + + @Test + void click_disabled_throws() { + view.radioButton.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + radioButton_::click); + } + + @Test + void click_disabledByProperty_throws() { + view.radioButton.setDisabled(true); + Assertions.assertThrows(IllegalStateException.class, + radioButton_::click); + } + + @Test + void click_invisible_throws() { + view.radioButton.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, + radioButton_::click); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/radiobutton/RadioButtonView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/radiobutton/RadioButtonView.java new file mode 100644 index 000000000..65a618f0d --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/radiobutton/RadioButtonView.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2000-2026 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.radiobutton; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "checkbox", registerAtStartup = false) +public class RadioButtonView extends Component implements HasComponents { + + RadioButton radioButton = new RadioButton<>("test", "item"); + + List items = Stream.of("foo", "bar", "baz", "jay").map(Name::new) + .collect(Collectors.toList()); + + RadioButtonGroup radioButtonGroup = new RadioButtonGroup<>(); + + public RadioButtonView() { + add(radioButton); + + radioButtonGroup.setItems(items); + radioButtonGroup.setItemLabelGenerator(item -> "test-" + item); + add(radioButtonGroup); + } + + public static class Name { + String name; + + public Name(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/AbstractTargetView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/AbstractTargetView.java new file mode 100644 index 000000000..d3d1efe6e --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/AbstractTargetView.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2000-2026 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.routerlink; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.html.Span; + +abstract class AbstractTargetView extends Component implements HasComponents { + + final Span message; + + public AbstractTargetView() { + message = new Span(); + add(message); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkQueryParameterTargetView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkQueryParameterTargetView.java new file mode 100644 index 000000000..ebd4b7f8c --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkQueryParameterTargetView.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2000-2026 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.routerlink; + +import java.util.stream.Collectors; + +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.Route; + +@Tag(Tag.DIV) +@Route(value = RouterLinkQueryParameterTargetView.ROUTE, registerAtStartup = false) +public class RouterLinkQueryParameterTargetView extends AbstractTargetView + implements BeforeEnterObserver { + + public static final String ROUTE = "router-link-query-parameter-target"; + + @Override + public void beforeEnter(BeforeEnterEvent event) { + var queryParameters = event.getLocation().getQueryParameters(); + message.setText( + "Query Parameter Target View: { " + + queryParameters.getParameters().entrySet().stream() + .map(entry -> entry.getKey() + " = [" + + entry.getValue().stream().sorted() + .collect(Collectors + .joining(", ")) + + "]") + .sorted().collect(Collectors.joining("; ")) + + " }"); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkRouteParameterTargetView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkRouteParameterTargetView.java new file mode 100644 index 000000000..b9b5fffc8 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkRouteParameterTargetView.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2000-2026 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.routerlink; + +import java.util.stream.Collectors; + +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.Route; + +@Tag(Tag.DIV) +@Route(value = RouterLinkRouteParameterTargetView.ROUTE + + RouterLinkRouteParameterTargetView.ROUTE_PARAMETERS, registerAtStartup = false) +public class RouterLinkRouteParameterTargetView extends AbstractTargetView + implements BeforeEnterObserver { + + public static final String ROUTE = "router-link-route-parameter-target"; + public static final String ROUTE_PARAMETERS = "/:segment1?/static/:segment2/:segment3*"; + + @Override + public void beforeEnter(BeforeEnterEvent event) { + var routeParameters = event.getRouteParameters(); + + message.setText( + "Route Parameter Target View: { " + + routeParameters.getParameterNames().stream() + .map(name -> name + " = " + + routeParameters.get(name).orElse("")) + .sorted().collect(Collectors.joining("; ")) + + " }"); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkStaticTargetView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkStaticTargetView.java new file mode 100644 index 000000000..4fdb6168f --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkStaticTargetView.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2000-2026 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.routerlink; + +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag(Tag.DIV) +@Route(value = RouterLinkStaticTargetView.ROUTE, registerAtStartup = false) +public class RouterLinkStaticTargetView extends AbstractTargetView + implements HasComponents { + + public static final String ROUTE = "router-link-static-target"; + + public RouterLinkStaticTargetView() { + message.setText("Static Target View"); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkTesterTest.java new file mode 100644 index 000000000..df9e8cc62 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkTesterTest.java @@ -0,0 +1,157 @@ +/** + * Copyright (C) 2000-2026 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.routerlink; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; +import com.vaadin.flow.router.RouterLink; + +@ViewPackages +class RouterLinkTesterTest extends BrowserlessTest { + + private RouterLinkView routerLinkView; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(RouterLinkView.class); + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(RouterLinkStaticTargetView.class); + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(RouterLinkUrlParameterTargetView.class); + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(RouterLinkQueryParameterTargetView.class); + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(RouterLinkRouteParameterTargetView.class); + + routerLinkView = navigate(RouterLinkView.class); + } + + @Test + void routerLink_targetless() { + // get router link + var $targetlessRouterLink = test(routerLinkView.targetlessRouterLink); + Assertions.assertNotNull($targetlessRouterLink, + "Tester for targetless RouterLink not initialized."); + + // verify its href + Assertions.assertEquals("", $targetlessRouterLink.getHref()); + + // verify its click action fails due to no navigation target + Assertions.assertThrows(IllegalStateException.class, + $targetlessRouterLink::click); + } + + @Test + void routerLink_static() { + // get router link + var $staticRouterLink = test(routerLinkView.staticRouterLink); + Assertions.assertNotNull($staticRouterLink, + "Tester for static RouterLink not initialized."); + + // verify its href + Assertions.assertEquals(RouterLinkStaticTargetView.ROUTE, + $staticRouterLink.getHref()); + + assertNavigationSucceeded($staticRouterLink, + RouterLinkStaticTargetView.class, "Static Target View"); + } + + @Test + void routerLink_emptyUrlParameter() { + var $emptyUrlParameterRouterLink = test( + routerLinkView.emptyUrlParameterRouterLink); + Assertions.assertNotNull($emptyUrlParameterRouterLink, + "Tester for empty URL parameter RouterLink not initialized."); + + // verify its href + Assertions.assertEquals(RouterLinkUrlParameterTargetView.ROUTE, + $emptyUrlParameterRouterLink.getHref()); + + assertNavigationSucceeded($emptyUrlParameterRouterLink, + RouterLinkUrlParameterTargetView.class, + "URL Parameter Target View: { }"); + } + + @Test + void routerLink_urlParameter() { + var $urlParameterRouterLink = test( + routerLinkView.urlParameterRouterLink); + Assertions.assertNotNull($urlParameterRouterLink, + "Tester for URL parameter RouterLink not initialized."); + + // verify its href + Assertions.assertEquals( + RouterLinkUrlParameterTargetView.ROUTE + "/parameter-value", + $urlParameterRouterLink.getHref()); + + assertNavigationSucceeded($urlParameterRouterLink, + RouterLinkUrlParameterTargetView.class, + "URL Parameter Target View: { parameter-value }"); + } + + @Test + void routerLink_queryParameter() { + var $queryParameterRouterLink = test( + routerLinkView.queryParameterRouterLink); + Assertions.assertNotNull($queryParameterRouterLink, + "Tester for QueryParameter RouterLink not initialized."); + + // verify its href + Assertions.assertEquals(RouterLinkQueryParameterTargetView.ROUTE + + "?parameter2=parameter2-value1¶meter2=parameter2-value2¶meter1=parameter1-value", + $queryParameterRouterLink.getHref()); + + assertNavigationSucceeded($queryParameterRouterLink, + RouterLinkQueryParameterTargetView.class, + "Query Parameter Target View: { parameter1 = [parameter1-value]; parameter2 = [parameter2-value1, parameter2-value2] }"); + } + + @Test + void routerLink_routeParameter() { + var $routeParameterRouterLink = test( + routerLinkView.routeParameterRouterLink); + Assertions.assertNotNull($routeParameterRouterLink, + "Tester for RouteParameter RouterLink not initialized."); + + // verify its href + Assertions.assertEquals(RouterLinkRouteParameterTargetView.ROUTE + + "/static/segment2-value/segment3-value1/segment3-value2", + $routeParameterRouterLink.getHref()); + + assertNavigationSucceeded($routeParameterRouterLink, + RouterLinkRouteParameterTargetView.class, + "Route Parameter Target View: { segment2 = segment2-value; segment3 = segment3-value1/segment3-value2 }"); + } + + private void assertNavigationSucceeded(RouterLinkTester tester, + Class expectedTarget, + String expectedMessage) { + // verify its navigate action returns correct target + var targetView = tester.navigate(); + Assertions.assertInstanceOf(expectedTarget, targetView); + Assertions.assertSame(targetView, getCurrentView()); + + // verify navigation target is correct + Assertions.assertEquals(expectedMessage, + expectedTarget.cast(targetView).message.getText()); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkUrlParameterTargetView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkUrlParameterTargetView.java new file mode 100644 index 000000000..119e349cf --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkUrlParameterTargetView.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2000-2026 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.routerlink; + +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.BeforeEvent; +import com.vaadin.flow.router.HasUrlParameter; +import com.vaadin.flow.router.OptionalParameter; +import com.vaadin.flow.router.Route; + +@Tag(Tag.DIV) +@Route(value = RouterLinkUrlParameterTargetView.ROUTE, registerAtStartup = false) +public class RouterLinkUrlParameterTargetView extends AbstractTargetView + implements HasComponents, HasUrlParameter { + + public static final String ROUTE = "router-link-url-parameter-target"; + + @Override + public void setParameter(BeforeEvent event, + @OptionalParameter String parameter) { + message.setText("URL Parameter Target View: { " + + (parameter != null ? parameter : "") + " }"); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkView.java new file mode 100644 index 000000000..72970dbd9 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/routerlink/RouterLinkView.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2000-2026 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.routerlink; + +import java.util.Map; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.QueryParameters; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParameters; +import com.vaadin.flow.router.RouterLink; + +@Tag(Tag.DIV) +@Route(value = RouterLinkView.ROUTE, registerAtStartup = false) +public class RouterLinkView extends Component implements HasComponents { + + public static final String ROUTE = "router-link-test"; + + final RouterLink targetlessRouterLink; + final RouterLink staticRouterLink; + final RouterLink emptyUrlParameterRouterLink; + final RouterLink urlParameterRouterLink; + final RouterLink queryParameterRouterLink; + final RouterLink routeParameterRouterLink; + + public RouterLinkView() { + // targetless router link + targetlessRouterLink = new RouterLink(); + targetlessRouterLink.setText("No Target"); + + // static router link + staticRouterLink = new RouterLink("Static Target", + RouterLinkStaticTargetView.class); + + // url parameter router link - empty + emptyUrlParameterRouterLink = new RouterLink( + "Empty URL Parameter Target", + RouterLinkUrlParameterTargetView.class); + + // url parameter router link - non-empty + urlParameterRouterLink = new RouterLink("URL Parameter Target", + RouterLinkUrlParameterTargetView.class, "parameter-value"); + + // query parameter router link + queryParameterRouterLink = new RouterLink("Query Parameter Target", + RouterLinkQueryParameterTargetView.class); + queryParameterRouterLink.setQueryParameters(QueryParameters.empty() + .merging("parameter1", "parameter1-value").merging("parameter2", + "parameter2-value1", "parameter2-value2")); + + // route parameter router link + routeParameterRouterLink = new RouterLink("Route Parameter Target", + RouterLinkRouteParameterTargetView.class, + new RouteParameters( + Map.ofEntries(Map.entry("segment2", "segment2-value"), + Map.entry("segment3", + "segment3-value1/segment3-value2")))); + + add(targetlessRouterLink, staticRouterLink, emptyUrlParameterRouterLink, + urlParameterRouterLink, queryParameterRouterLink, + routeParameterRouterLink); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/select/SelectTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/select/SelectTesterTest.java new file mode 100644 index 000000000..87c3c4d28 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/select/SelectTesterTest.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2000-2026 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.select; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +import static org.junit.jupiter.api.Assertions.assertIterableEquals; + +@ViewPackages +class SelectTesterTest extends BrowserlessTest { + SelectView view; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(SelectView.class); + view = navigate(SelectView.class); + } + + @Test + void getSuggestionItems_returnsAllItems() { + final SelectTester, String> select_ = test(view.select); + assertIterableEquals(view.items, select_.getSuggestionItems()); + + final SelectTester, SelectView.Person> person_ = test( + view.personSelect); + assertIterableEquals(view.people, person_.getSuggestionItems()); + } + + @Test + void stringSelect_getSuggestions_valuesEqualItems() { + final SelectTester, String> select_ = test(view.select); + assertIterableEquals(view.items, select_.getSuggestions()); + } + + @Test + void stringSelect_selectItem_selectsCorrectItem() { + final SelectTester, String> select_ = test(view.select); + Assertions.assertNull(select_.getSelected()); + + select_.selectItem("Fantasy"); + + Assertions.assertSame(view.items.get(2), select_.getSelected()); + + select_.selectItem(null); + + Assertions.assertNull(select_.getSelected(), + "Selecting null should clear selection"); + } + + @Test + void beanSelect_selectItem_selectsCorrectItem() { + final SelectTester, SelectView.Person> select_ = test( + view.personSelect); + Assertions.assertNull(select_.getSelected()); + + select_.selectItem("Space"); + + Assertions.assertSame(view.people.get(1), select_.getSelected()); + + select_.selectItem(null); + + Assertions.assertNull(select_.getSelected(), + "Selecting null should clear selection"); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/select/SelectView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/select/SelectView.java new file mode 100644 index 000000000..e190bb8e9 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/select/SelectView.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2000-2026 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.select; + +import java.util.Arrays; +import java.util.List; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "select", registerAtStartup = false) +public class SelectView extends Component implements HasComponents { + Select select; + Select personSelect; + List people = Arrays.asList(new Person("John", "Doe"), + new Person("Space", "Cat")); + List items = Arrays.asList("Good", "Omens", "Fantasy", "Drawing"); + + public SelectView() { + select = new Select<>(); + select.setItems(items); + + personSelect = new Select<>(); + personSelect.setItemLabelGenerator(Person::getFirst); + personSelect.setItems(people); + + add(select, personSelect); + } + + static class Person { + String first; + String last; + + public Person(String first, String last) { + this.first = first; + this.last = last; + } + + public String getFirst() { + return first; + } + + public String getLast() { + return last; + } + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/sidenav/SideNavTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/sidenav/SideNavTesterTest.java new file mode 100644 index 000000000..0491528b0 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/sidenav/SideNavTesterTest.java @@ -0,0 +1,329 @@ +/** + * Copyright (C) 2000-2026 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.sidenav; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.component.HasElement; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class SideNavTesterTest extends BrowserlessTest { + + SideNavView view; + SideNavTester sideNav_; + + @BeforeEach + void init() { + RouteConfiguration routeConfiguration = RouteConfiguration + .forApplicationScope(); + routeConfiguration.setAnnotatedRoute(SideNavView.class); + routeConfiguration.setAnnotatedRoute(TargetView.class); + navigateToSideNavView(); + } + + @Test + void sideNav_notVisible_throws() { + view.sideNav.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, sideNav_::click); + Assertions.assertThrows(IllegalStateException.class, + () -> sideNav_.clickItem("Messages")); + } + + @Test + void sideNav_notAttached_throws() { + view.sideNav.removeFromParent(); + Assertions.assertThrows(IllegalStateException.class, sideNav_::click); + Assertions.assertThrows(IllegalStateException.class, + () -> sideNav_.clickItem("Messages")); + } + + @Test + void click_collapsible_sideNavExpandsAndCollapse() { + view.sideNav.setCollapsible(true); + Assertions.assertTrue(view.sideNav.isExpanded(), + "Expected SideNav to be expanded by default"); + sideNav_.click(); + Assertions.assertFalse(view.sideNav.isExpanded(), + "Expected SideNav to be collapsed after click on SideNav top"); + sideNav_.click(); + Assertions.assertTrue(view.sideNav.isExpanded(), + "Expected SideNav to be expanded again after second click on SideNav top"); + } + + @Test + void click_notCollapsible_noAction() { + view.sideNav.setCollapsible(false); + Assertions.assertTrue(view.sideNav.isExpanded(), + "Expected SideNav to be expanded by default"); + sideNav_.click(); + Assertions.assertTrue(view.sideNav.isExpanded(), + "Expected SideNav to be expanded because click should have no effect"); + sideNav_.click(); + Assertions.assertTrue(view.sideNav.isExpanded(), + "Expected SideNav to be expanded because click should have no effect"); + } + + @Test + void toggle_collapsible_sideNavExpandsAndCollapse() { + view.sideNav.setCollapsible(true); + Assertions.assertTrue(view.sideNav.isExpanded(), + "Expected SideNav to be expanded by default"); + sideNav_.toggle(); + Assertions.assertFalse(view.sideNav.isExpanded(), + "Expected SideNav to be collapsed after click on SideNav top"); + sideNav_.toggle(); + Assertions.assertTrue(view.sideNav.isExpanded(), + "Expected SideNav to be expanded again after second click on SideNav top"); + } + + @Test + void toggle_notCollapsible_throws() { + view.sideNav.setCollapsible(false); + Assertions.assertThrows(IllegalStateException.class, sideNav_::toggle); + } + + @Test + void clickItem_byText_navigationHappens() { + sideNav_.clickItem("Messages"); + assertNavigatedToTargetViewWithParam("N/A"); + } + + @Test + void clickItem_byText_leafWithoutPath_noNavigation() { + sideNav_.clickItem("No Link"); + assertNoNavigation(); + } + + @Test + void clickItem_byText_parentWithoutPath_expandsAndCollapse() { + Assertions.assertFalse(view.adminSection.isExpanded(), + "Expected SideNavItem to be collapsed by default"); + sideNav_.clickItem("Admin"); + Assertions.assertTrue(view.adminSection.isExpanded(), + "Expected SideNavItem to be expanded after click"); + sideNav_.clickItem("Admin"); + Assertions.assertFalse(view.adminSection.isExpanded(), + "Expected SideNav to be collapsed again after second click"); + } + + @Test + void clickItem_notExisting_throws() { + String label = "Not existing item"; + IllegalArgumentException exception = Assertions.assertThrowsExactly( + IllegalArgumentException.class, + () -> sideNav_.clickItem(label)); + Assertions.assertTrue(exception.getMessage() + .contains("Cannot find SideNav item '" + label + "'")); + } + + @Test + void clickItem_multipleMatches_throws() { + IllegalStateException exception = Assertions.assertThrows( + IllegalStateException.class, + () -> sideNav_.clickItem("Top Level Duplicated")); + Assertions.assertTrue(exception.getMessage() + .contains("Found 2 items with label 'Top Level Duplicated'")); + } + + @Test + void clickItem_hidden_throws() { + view.adminSection.setVisible(false); + IllegalStateException exception = Assertions.assertThrows( + IllegalStateException.class, () -> sideNav_.clickItem("Admin")); + Assertions.assertTrue(exception.getMessage().contains("SideNavItem")); + Assertions.assertTrue(exception.getMessage().contains("is not usable")); + } + + @Test + void clickItem_nested_navigationHappens() { + view.adminSection.setExpanded(true); + view.securitySection.setExpanded(true); + sideNav_.clickItem("Admin", "Security", "Users"); + assertNavigatedToTargetViewWithParam("users"); + } + + @Test + void clickItem_nestedItemHidden_throws() { + view.adminSection.setExpanded(true); + view.securitySection.setVisible(false); + IllegalStateException exception = Assertions.assertThrowsExactly( + IllegalStateException.class, + () -> sideNav_.clickItem("Admin", "Security", "Users")); + Assertions.assertTrue(exception.getMessage().contains("SideNavItem")); + Assertions.assertTrue(exception.getMessage().contains("Security")); + Assertions.assertTrue(exception.getMessage().contains("is not usable")); + } + + @Test + void clickItem_nestedItem_parentCollapsed_throws() { + view.adminSection.setExpanded(false); + view.securitySection.setExpanded(false); + IllegalStateException exception = Assertions.assertThrowsExactly( + IllegalStateException.class, + () -> sideNav_.clickItem("Admin", "Security", "Users")); + Assertions.assertTrue(exception.getMessage() + .contains("Cannot find SideNav item with label 'Security'")); + Assertions.assertTrue(exception.getMessage() + .contains("on path 'Admin / Security / Users'")); + Assertions.assertTrue(exception.getMessage() + .contains("parent item 'Admin' is collapsed")); + } + + @Test + void clickItem_nestedWrongPath_throws() { + view.adminSection.setExpanded(true); + IllegalArgumentException exception = Assertions.assertThrowsExactly( + IllegalArgumentException.class, + () -> sideNav_.clickItem("Admin", "Configuration", "Users")); + Assertions.assertTrue(exception.getMessage().contains( + "Cannot find SideNav item 'Configuration' on path Admin / Configuration / Users")); + } + + @Test + void clickItem_multipleNestedMatches_throws() { + IllegalStateException exception = Assertions.assertThrows( + IllegalStateException.class, + () -> sideNav_.clickItem("Nested duplicated", "Duplicated")); + Assertions.assertTrue(exception.getMessage() + .contains("Found 2 items with label 'Duplicated'")); + Assertions.assertTrue(exception.getMessage() + .contains("on path Nested duplicated / Duplicated")); + } + + @Test + void expandAndClickItem_nested_expandCollapsedItems() { + sideNav_.expandAndClickItem("Admin", "Security", "Users"); + assertNavigatedToTargetViewWithParam("users"); + + Assertions.assertTrue(view.adminSection.isExpanded(), + "Expected 'Admin' item to be expanded"); + Assertions.assertTrue(view.securitySection.isExpanded(), + "Expected 'Admin / Security' item to be expanded"); + } + + @Test + void toggleItem_expandsAndCollapse() { + Assertions.assertFalse(view.adminSection.isExpanded(), + "Expected Admin SideNavItem to be collapsed by default"); + sideNav_.toggleItem("Admin"); + Assertions.assertTrue(view.adminSection.isExpanded(), + "Expected Admin SideNavItem to be expanded after click"); + sideNav_.toggleItem("Admin"); + Assertions.assertFalse(view.adminSection.isExpanded(), + "Expected Admin SideNavItem to be collapsed again after second click"); + } + + @Test + void toggleItem_nestedItem_expandsAndCollapse() { + view.adminSection.setExpanded(true); + Assertions.assertFalse(view.securitySection.isExpanded(), + "Expected Security SideNavItem to be collapsed by default"); + sideNav_.toggleItem("Admin", "Security"); + Assertions.assertTrue(view.securitySection.isExpanded(), + "Expected Security SideNavItem to be expanded after click"); + sideNav_.toggleItem("Admin", "Security"); + Assertions.assertFalse(view.securitySection.isExpanded(), + "Expected Security SideNavItem to be collapsed again after second click"); + } + + @Test + void toggleItem_nestedItemHidden_throws() { + view.adminSection.setExpanded(true); + view.securitySection.setVisible(false); + IllegalStateException exception = Assertions.assertThrowsExactly( + IllegalStateException.class, + () -> sideNav_.toggleItem("Admin", "Security")); + Assertions.assertTrue(exception.getMessage().contains("SideNavItem")); + Assertions.assertTrue(exception.getMessage().contains("Security")); + Assertions.assertTrue(exception.getMessage().contains("is not usable")); + } + + @Test + void toggleItem_nestedItem_parentCollapsed_throws() { + view.adminSection.setExpanded(false); + view.securitySection.setExpanded(false); + IllegalStateException exception = Assertions.assertThrowsExactly( + IllegalStateException.class, + () -> sideNav_.toggleItem("Admin", "Security")); + Assertions.assertTrue(exception.getMessage() + .contains("Cannot find SideNav item with label 'Security'")); + Assertions.assertTrue( + exception.getMessage().contains("on path 'Admin / Security")); + Assertions.assertTrue(exception.getMessage() + .contains("parent item 'Admin' is collapsed")); + } + + @Test + void toggleItem_nestedWrongPath_throws() { + view.adminSection.setExpanded(true); + IllegalArgumentException exception = Assertions.assertThrowsExactly( + IllegalArgumentException.class, + () -> sideNav_.toggleItem("Admin", "Configuration", "Users")); + Assertions.assertTrue(exception.getMessage().contains( + "Cannot find SideNav item 'Configuration' on path Admin / Configuration / Users")); + } + + @Test + void toggleItem_leafItem_throws() { + IllegalStateException exception = Assertions.assertThrows( + IllegalStateException.class, + () -> sideNav_.toggleItem("No Link")); + Assertions.assertTrue(exception.getMessage() + .contains("Toggle button cannot be clicked")); + + view.adminSection.setExpanded(true); + view.securitySection.setExpanded(true); + exception = Assertions.assertThrows(IllegalStateException.class, + () -> sideNav_.toggleItem("Admin", "Security", "Users")); + Assertions.assertTrue(exception.getMessage() + .contains("Toggle button cannot be clicked")); + } + + @Test + void toggleItem_multipleMatches_throws() { + IllegalStateException exception = Assertions.assertThrows( + IllegalStateException.class, + () -> sideNav_.toggleItem("Nested duplicated", "Duplicated")); + Assertions.assertTrue(exception.getMessage().contains( + "Found 2 items with label 'Duplicated' on path Nested duplicated / Duplicated")); + } + + private void assertNavigatedToTargetViewWithParam( + String expectedParamValue) { + HasElement currentView = getCurrentView(); + Assertions.assertInstanceOf(TargetView.class, currentView, + "Expecting click on SideNav item to navigate to TargetView, but current view is " + + currentView.getClass()); + Assertions.assertEquals(((TargetView) currentView).parameter, + expectedParamValue); + } + + private void assertNoNavigation() { + HasElement currentView = getCurrentView(); + Assertions.assertSame(view, currentView, + "Expecting click on SideNav item without path to stay on current view"); + } + + private void navigateToSideNavView() { + view = navigate(SideNavView.class); + sideNav_ = test(view.sideNav); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/sidenav/SideNavView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/sidenav/SideNavView.java new file mode 100644 index 000000000..5bfceffdf --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/sidenav/SideNavView.java @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2000-2026 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.sidenav; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParameters; + +@Tag("div") +@Route(value = "sidenav", registerAtStartup = false) +public class SideNavView extends Component implements HasComponents { + + final SideNav sideNav; + final SideNavItem adminSection; + final SideNavItem securitySection; + + public SideNavView() { + sideNav = new SideNav("Menu"); + SideNavItem messagesLink = new SideNavItem("Messages", TargetView.class, + VaadinIcon.ENVELOPE.create()); + messagesLink.addItem(new SideNavItem("Inbox", TargetView.class, + new RouteParameters("param", "inbox"), + VaadinIcon.INBOX.create())); + messagesLink.addItem(new SideNavItem("Sent", TargetView.class, + new RouteParameters("param", "sent"), + VaadinIcon.PAPERPLANE.create())); + messagesLink.addItem(new SideNavItem("Trash", "sidenav-target/trash", + VaadinIcon.TRASH.create())); + + adminSection = new SideNavItem("Admin"); + adminSection.setPrefixComponent(VaadinIcon.COG.create()); + securitySection = new SideNavItem("Security"); + adminSection.addItem(securitySection); + + securitySection.addItem(new SideNavItem("Users", TargetView.class, + new RouteParameters("param", "users"), + VaadinIcon.GROUP.create())); + securitySection.addItem(new SideNavItem("Permissions", TargetView.class, + new RouteParameters("param", "permissions"), + VaadinIcon.KEY.create())); + securitySection.addItem(new SideNavItem("No Op")); + + sideNav.addItem(messagesLink, adminSection); + sideNav.addItem(new SideNavItem("No Link")); + + // Duplicated items + sideNav.addItem(new SideNavItem("Top Level Duplicated")); + sideNav.addItem(new SideNavItem("Top Level Duplicated")); + SideNavItem nestedDuplicates = new SideNavItem("Nested duplicated"); + nestedDuplicates.setExpanded(true); + nestedDuplicates.addItem(new SideNavItem("Duplicated"), + new SideNavItem("Duplicated")); + sideNav.addItem(nestedDuplicates); + + add(sideNav); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/sidenav/TargetView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/sidenav/TargetView.java new file mode 100644 index 000000000..ede403015 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/sidenav/TargetView.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2000-2026 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.sidenav; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteAlias; + +@Tag("div") +@Route(value = "sidenav-target", registerAtStartup = false) +@RouteAlias("sidenav-target/:param") +public class TargetView extends Component implements BeforeEnterObserver { + + String parameter; + + @Override + public void beforeEnter(BeforeEnterEvent event) { + parameter = event.getRouteParameters().get("param").orElse("N/A"); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabSheetTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabSheetTesterTest.java new file mode 100644 index 000000000..1415fb6cb --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabSheetTesterTest.java @@ -0,0 +1,265 @@ +/** + * Copyright (C) 2000-2026 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.tabs; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class TabSheetTesterTest extends BrowserlessTest { + + TabSheetView view; + TabSheetTester tabs_; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(TabSheetView.class); + view = navigate(TabSheetView.class); + tabs_ = test(view.tabs); + } + + @Test + void select_notUsable_throws() { + view.tabs.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.select("Details")); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.select(0)); + } + + @Test + void select_disabledTab_throws() { + view.payment.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.select("Payment")); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.select(1)); + } + + @Test + void select_hiddenTab_throws() { + view.payment.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.select("Payment")); + } + + @Test + void select_notExistingTab_throws() { + view.payment.setEnabled(false); + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.select("Summary")); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.select(4)); + } + + @Test + void select_byLabel_selectsTab() { + AtomicReference selectedTab = new AtomicReference<>(); + view.tabs.addSelectedChangeListener( + ev -> selectedTab.set(ev.getSelectedTab())); + + tabs_.select("Payment"); + Assertions.assertEquals(view.payment, selectedTab.get()); + + tabs_.select("Details"); + Assertions.assertEquals(view.details, selectedTab.get()); + + tabs_.select("Shipping"); + Assertions.assertEquals(view.shipping, selectedTab.get()); + } + + @Test + void select_byIndex_selectsTab() { + AtomicReference selectedTab = new AtomicReference<>(); + view.tabs.addSelectedChangeListener( + ev -> selectedTab.set(ev.getSelectedTab())); + + tabs_.select(1); + Assertions.assertEquals(view.payment, selectedTab.get()); + + tabs_.select(0); + Assertions.assertEquals(view.details, selectedTab.get()); + + tabs_.select(2); + Assertions.assertEquals(view.shipping, selectedTab.get()); + + tabs_.select(-1); + Assertions.assertNull(selectedTab.get()); + } + + @Test + void select_byIndex_indexRefersToVisibleTabs() { + view.details.setVisible(false); + AtomicReference selectedTab = new AtomicReference<>(); + view.tabs.addSelectedChangeListener( + ev -> selectedTab.set(ev.getSelectedTab())); + + // Payment now is the first visible tab + tabs_.select(0); + Assertions.assertEquals(view.payment, selectedTab.get()); + + view.payment.setVisible(false); + // Shipping is now is the only visible tab + tabs_.select(0); + Assertions.assertEquals(view.shipping, selectedTab.get()); + } + + @Test + void getTab_getsCorrectTab() { + Assertions.assertSame(view.details, tabs_.getTab("Details")); + Assertions.assertSame(view.payment, tabs_.getTab("Payment")); + Assertions.assertSame(view.shipping, tabs_.getTab("Shipping")); + + Assertions.assertSame(view.details, tabs_.getTab(0)); + Assertions.assertSame(view.payment, tabs_.getTab(1)); + Assertions.assertSame(view.shipping, tabs_.getTab(2)); + + view.details.setVisible(false); + // Payment now is the first visible tab + Assertions.assertSame(view.payment, tabs_.getTab(0)); + + view.payment.setVisible(false); + // Shipping is now is the only visible tab + Assertions.assertSame(view.shipping, tabs_.getTab(0)); + + // Now Details and Shipping + view.details.setVisible(true); + Assertions.assertSame(view.details, tabs_.getTab(0)); + Assertions.assertSame(view.shipping, tabs_.getTab(1)); + } + + @Test + void getTab_notExistingTab_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.getTab("Summary")); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.getTab(4)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.getTab(-1)); + } + + @Test + void getTab_hidden_throws() { + view.payment.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.getTab("Payment")); + } + + @Test + void isSelected_getsTabState() { + view.tabs.setSelectedIndex(1); + + Assertions.assertFalse(tabs_.isSelected("Details"), + "Details tab is not selected, but got true"); + Assertions.assertTrue(tabs_.isSelected("Payment"), + "Payment tab is selected, but got false"); + Assertions.assertFalse(tabs_.isSelected("Shipping"), + "Shipping tab is not selected, but got true"); + + Assertions.assertFalse(tabs_.isSelected(0), + "Details tab at index 0 is not selected, but got true"); + Assertions.assertTrue(tabs_.isSelected(1), + "Payment tab at index 1 is selected, but got false"); + Assertions.assertFalse(tabs_.isSelected(2), + "Shipping tab at index 2 is not selected, but got true"); + } + + @Test + void isSelected_byIndex_invisibleTabsIgnored() { + view.payment.setVisible(false); + view.tabs.setSelectedTab(view.shipping); + + Assertions.assertFalse(tabs_.isSelected(0), + "Details tab at index 0 should be not selected, but got true"); + Assertions.assertTrue(tabs_.isSelected(1), + "Shipping tab at index 1 (payment hidden) should be selected, but got false"); + + view.payment.setVisible(true); + view.details.setVisible(false); + view.tabs.setSelectedTab(view.payment); + Assertions.assertTrue(tabs_.isSelected(0), + "Payment tab at index 0 (details hidden) should be selected, but got false"); + Assertions.assertFalse(tabs_.isSelected(1), + "Shipping tab at index 1 (details hidden) should be not selected, but got true"); + } + + @Test + void isSelected_notExistingTab_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.isSelected("Summary")); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.isSelected(4)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.isSelected(-1)); + } + + @Test + void getTabContent_getsCorrectTab() { + Assertions.assertSame(view.detailsContent, + tabs_.getTabContent("Details")); + Assertions.assertSame(view.paymentContent, + tabs_.getTabContent("Payment")); + Assertions.assertSame(view.shippingContent, + tabs_.getTabContent("Shipping")); + + Assertions.assertSame(view.detailsContent, tabs_.getTabContent(0)); + Assertions.assertSame(view.paymentContent, tabs_.getTabContent(1)); + Assertions.assertSame(view.shippingContent, tabs_.getTabContent(2)); + + view.details.setVisible(false); + // Payment now is the first visible tab + Assertions.assertSame(view.paymentContent, tabs_.getTabContent(0)); + + view.payment.setVisible(false); + // Shipping is now is the only visible tab + Assertions.assertSame(view.shippingContent, tabs_.getTabContent(0)); + + // Now Details and Shipping + view.details.setVisible(true); + Assertions.assertSame(view.detailsContent, tabs_.getTabContent(0)); + Assertions.assertSame(view.shippingContent, tabs_.getTabContent(1)); + } + + @Test + void getTabContent_notExistingTab_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.getTabContent("Summary")); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.getTabContent(4)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.getTabContent(-1)); + } + + @Test + void getTabContent_hidden_throws() { + view.payment.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.getTabContent("Payment")); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabSheetView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabSheetView.java new file mode 100644 index 000000000..69a8eb920 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabSheetView.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2000-2026 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.tabs; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "tabsheet", registerAtStartup = false) +public class TabSheetView extends Component implements HasComponents { + + TabSheet tabs; + Tab details; + Span detailsContent = new Span("Details contents"); + Tab payment; + Span paymentContent = new Span("Payment contents"); + + Tab shipping; + Span shippingContent = new Span("Shipping contents"); + + public TabSheetView() { + tabs = new TabSheet(); + details = tabs.add(new Tab("Details"), detailsContent); + payment = tabs.add("Payment", paymentContent); + shipping = tabs.add("Shipping", shippingContent); + add(tabs); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabsTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabsTesterTest.java new file mode 100644 index 000000000..1b5bd4fc0 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabsTesterTest.java @@ -0,0 +1,216 @@ +/** + * Copyright (C) 2000-2026 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.tabs; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class TabsTesterTest extends BrowserlessTest { + + TabsView view; + TabsTester tabs_; + + @BeforeEach + void init() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(TabsView.class); + view = navigate(TabsView.class); + tabs_ = test(view.tabs); + } + + @Test + void select_notUsable_throws() { + view.tabs.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.select("Details")); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.select(0)); + } + + @Test + void select_disableTabs_throws() { + view.payment.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.select("Payment")); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.select(1)); + } + + @Test + void select_notExistingTab_throws() { + view.payment.setEnabled(false); + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.select("Summary")); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.select(4)); + + } + + @Test + void select_byLabel_selectsTab() { + AtomicReference selectedTab = new AtomicReference<>(); + view.tabs.addSelectedChangeListener( + ev -> selectedTab.set(ev.getSelectedTab())); + + tabs_.select("Payment"); + Assertions.assertEquals(view.payment, selectedTab.get()); + + tabs_.select("Details"); + Assertions.assertEquals(view.details, selectedTab.get()); + + tabs_.select("Shipping"); + Assertions.assertEquals(view.shipping, selectedTab.get()); + } + + @Test + void select_byIndex_selectsTab() { + AtomicReference selectedTab = new AtomicReference<>(); + view.tabs.addSelectedChangeListener( + ev -> selectedTab.set(ev.getSelectedTab())); + + tabs_.select(1); + Assertions.assertEquals(view.payment, selectedTab.get()); + + tabs_.select(0); + Assertions.assertEquals(view.details, selectedTab.get()); + + tabs_.select(2); + Assertions.assertEquals(view.shipping, selectedTab.get()); + + tabs_.select(-1); + Assertions.assertNull(selectedTab.get()); + + } + + @Test + void select_byIndex_indexRefersToVisibleTabs() { + view.details.setVisible(false); + AtomicReference selectedTab = new AtomicReference<>(); + view.tabs.addSelectedChangeListener( + ev -> selectedTab.set(ev.getSelectedTab())); + + // Payment now is the first visible tab + tabs_.select(0); + Assertions.assertEquals(view.payment, selectedTab.get()); + + view.payment.setVisible(false); + // Shipping is now is the only visible tab + tabs_.select(0); + Assertions.assertEquals(view.shipping, selectedTab.get()); + } + + @Test + void getTab_getsCorrectTab() { + Assertions.assertSame(view.details, tabs_.getTab("Details")); + Assertions.assertSame(view.payment, tabs_.getTab("Payment")); + Assertions.assertSame(view.shipping, tabs_.getTab("Shipping")); + + Assertions.assertSame(view.details, tabs_.getTab(0)); + Assertions.assertSame(view.payment, tabs_.getTab(1)); + Assertions.assertSame(view.shipping, tabs_.getTab(2)); + + view.details.setVisible(false); + // Payment now is the first visible tab + Assertions.assertSame(view.payment, tabs_.getTab(0)); + + view.payment.setVisible(false); + // Shipping is now is the only visible tab + Assertions.assertSame(view.shipping, tabs_.getTab(0)); + + // Now Details and Shipping + view.details.setVisible(true); + Assertions.assertSame(view.details, tabs_.getTab(0)); + Assertions.assertSame(view.shipping, tabs_.getTab(1)); + + } + + @Test + void getTab_notExistingTab_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.getTab("Summary")); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.getTab(4)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.getTab(-1)); + } + + @Test + void getTab_hidden_throws() { + view.payment.setVisible(false); + Assertions.assertThrows(IllegalStateException.class, + () -> tabs_.getTab("Payment")); + } + + @Test + void isSelected_getsTabState() { + view.tabs.setSelectedIndex(1); + + Assertions.assertFalse(tabs_.isSelected("Details"), + "Details tab is not selected, but got true"); + Assertions.assertTrue(tabs_.isSelected("Payment"), + "Payment tab is selected, but got false"); + Assertions.assertFalse(tabs_.isSelected("Shipping"), + "Shipping tab is not selected, but got true"); + + Assertions.assertFalse(tabs_.isSelected(0), + "Details tab at index 0 is not selected, but got true"); + Assertions.assertTrue(tabs_.isSelected(1), + "Payment tab at index 1 is selected, but got false"); + Assertions.assertFalse(tabs_.isSelected(2), + "Shipping tab at index 2 is not selected, but got true"); + } + + @Test + void isSelected_byIndex_invisibleTabsIgnored() { + view.payment.setVisible(false); + view.tabs.setSelectedTab(view.shipping); + + Assertions.assertFalse(tabs_.isSelected(0), + "Details tab at index 0 should be not selected, but got true"); + Assertions.assertTrue(tabs_.isSelected(1), + "Shipping tab at index 1 (payment hidden) should be selected, but got false"); + + view.payment.setVisible(true); + view.details.setVisible(false); + view.tabs.setSelectedTab(view.payment); + Assertions.assertTrue(tabs_.isSelected(0), + "Payment tab at index 0 (details hidden) should be selected, but got false"); + Assertions.assertFalse(tabs_.isSelected(1), + "Shipping tab at index 1 (details hidden) should be not selected, but got true"); + } + + @Test + void isSelected_notExistingTab_throws() { + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.isSelected("Summary")); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.isSelected(4)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> tabs_.isSelected(-1)); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabsView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabsView.java new file mode 100644 index 000000000..f7e80f880 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/tabs/TabsView.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2000-2026 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.tabs; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "tabs", registerAtStartup = false) +public class TabsView extends Component implements HasComponents { + + Tabs tabs; + Tab details; + Tab payment; + Tab shipping; + + public TabsView() { + details = new Tab("Details"); + payment = new Tab("Payment"); + shipping = new Tab("Shipping"); + + tabs = new Tabs(details, payment, shipping); + add(tabs); + } +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/textfield/NumberFieldTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/textfield/NumberFieldTesterTest.java new file mode 100644 index 000000000..816b73b4b --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/textfield/NumberFieldTesterTest.java @@ -0,0 +1,143 @@ +/** + * Copyright (C) 2000-2026 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.textfield; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.component.AbstractField; +import com.vaadin.flow.component.HasValue; +import com.vaadin.flow.router.RouteConfiguration; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ViewPackages +class NumberFieldTesterTest extends BrowserlessTest { + + private NumberFieldView view; + + @BeforeEach + public void registerView() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(NumberFieldView.class); + view = navigate(NumberFieldView.class); + } + + @Test + public void readOnlyNumberField_isNotUsable() { + view.numberField.setReadOnly(true); + + final NumberFieldTester nf_ = test( + view.numberField); + + Assertions.assertFalse(nf_.isUsable(), + "Read only NumberField shouldn't be usable"); + } + + @Test + public void readOnlyNumberField_automatictester_readOnlyIsCheckedInUsable() { + view.numberField.setReadOnly(true); + + Assertions.assertFalse(test(view.numberField).isUsable(), + "Read only NumberField shouldn't be usable"); + } + + @Test + public void setNumberFieldValue_eventIsFired_valueIsSet() { + + AtomicReference value = new AtomicReference<>(null); + + view.numberField.addValueChangeListener( + (HasValue.ValueChangeListener>) event -> { + if (event.isFromClient()) { + value.compareAndSet(null, event.getValue()); + } + }); + + final NumberFieldTester nf_ = test( + view.numberField); + final Double newValue = 15d; + nf_.setValue(newValue); + + Assertions.assertEquals(newValue, value.get()); + } + + @Test + public void setIntegerFieldValue_eventIsFired_valueIsSet() { + + AtomicReference value = new AtomicReference<>(null); + + view.integerField.addValueChangeListener( + (HasValue.ValueChangeListener>) event -> { + value.compareAndSet(null, event.getValue()); + }); + + final NumberFieldTester inf_ = test( + view.integerField); + final Integer newValue = 15; + inf_.setValue(newValue); + + Assertions.assertEquals(newValue, value.get()); + } + + @Test + public void nonInteractableField_throwsOnSetValue() { + + view.numberField.getElement().setEnabled(false); + view.integerField.getElement().setEnabled(false); + final NumberFieldTester nf_ = test( + view.numberField); + final NumberFieldTester inf_ = test( + view.integerField); + + Assertions.assertThrows(IllegalStateException.class, + () -> nf_.setValue(12d), + "Setting value to a non interactable number field should fail"); + Assertions.assertThrows(IllegalStateException.class, + () -> inf_.setValue(12), + "Setting value to a non interactable integer field should fail"); + } + + @Test + public void maxValue_throwsExceptionForTooSmallValue() { + view.numberField.setMax(10.0); + + final NumberFieldTester nf_ = test( + view.numberField); + final Double newValue = 15d; + + assertThrows(IllegalArgumentException.class, + () -> nf_.setValue(newValue)); + } + + @Test + public void minValue_throwsExceptionForTooSmallValue() { + view.numberField.setMin(20.0); + + final NumberFieldTester nf_ = test( + view.numberField); + final Double newValue = 15d; + + assertThrows(IllegalArgumentException.class, + () -> nf_.setValue(newValue)); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/textfield/NumberFieldView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/textfield/NumberFieldView.java new file mode 100644 index 000000000..4605d33c5 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/textfield/NumberFieldView.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2000-2026 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.textfield; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasComponents; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.Route; + +@Tag("div") +@Route(value = "number-fields", registerAtStartup = false) +public class NumberFieldView extends Component implements HasComponents { + NumberField numberField; + IntegerField integerField; + + public NumberFieldView() { + numberField = new NumberField(); + integerField = new IntegerField(); + + add(numberField, integerField); + } + +} diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/textfield/TextAreaTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/textfield/TextAreaTesterTest.java new file mode 100644 index 000000000..c62c6e617 --- /dev/null +++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/textfield/TextAreaTesterTest.java @@ -0,0 +1,147 @@ +/** + * Copyright (C) 2000-2026 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.textfield; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.browserless.BrowserlessTest; +import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.component.AbstractField; +import com.vaadin.flow.component.HasValue; +import com.vaadin.flow.router.RouteConfiguration; + +@ViewPackages +class TextAreaTesterTest extends BrowserlessTest { + + private TextAreaView view; + + @BeforeEach + public void registerView() { + RouteConfiguration.forApplicationScope() + .setAnnotatedRoute(TextAreaView.class); + view = navigate(TextAreaView.class); + } + + @Test + public void readOnlyTextArea_isNotUsable() { + view.textArea.setReadOnly(true); + + final TextAreaTester