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
+ *
+ *
> 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 {
+
+ }
+
+ @Tag("div")
+ private static class Div extends Component implements HasComponents {
+ public Div(Component... children) {
+ add(children);
+ }
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/ClickableTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/ClickableTest.java
new file mode 100644
index 000000000..3685a9a9e
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/ClickableTest.java
@@ -0,0 +1,204 @@
+/**
+ * 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 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.flow.component.ClickEvent;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.ComponentEventListener;
+import com.vaadin.flow.component.ComponentUtil;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Span;
+
+/**
+ * Tests for the Clickable mixin interface functionality.
+ */
+@ViewPackages(packages = "com.example")
+public class ClickableTest extends BrowserlessTest {
+
+ private Div container;
+
+ @BeforeEach
+ public void init() {
+ container = new Div();
+ getCurrentView().getElement().appendChild(container.getElement());
+ }
+
+ @Test
+ public void testClick_firesClickEvent() {
+ Div div = new Div("Click me");
+ container.add(div);
+
+ AtomicInteger clickCount = new AtomicInteger(0);
+ div.addClickListener(e -> clickCount.incrementAndGet());
+
+ ComponentTester divTester = test(div);
+ divTester.click();
+
+ Assertions.assertEquals(1, clickCount.get(),
+ "Click event should be fired once");
+ }
+
+ @Test
+ public void testClick_withMetaKeys() {
+ Span span = new Span("Click with meta keys");
+ container.add(span);
+
+ AtomicReference
> clickEvent = new AtomicReference<>();
+ span.addClickListener(clickEvent::set);
+
+ ComponentTester spanTester = test(span);
+ MetaKeys metaKeys = new MetaKeys().ctrl().shift();
+ spanTester.click(metaKeys);
+
+ Assertions.assertNotNull(clickEvent.get(),
+ "Click event should be fired");
+ Assertions.assertTrue(clickEvent.get().isCtrlKey(),
+ "Ctrl key should be pressed");
+ Assertions.assertTrue(clickEvent.get().isShiftKey(),
+ "Shift key should be pressed");
+ Assertions.assertFalse(clickEvent.get().isAltKey(),
+ "Alt key should not be pressed");
+ Assertions.assertFalse(clickEvent.get().isMetaKey(),
+ "Meta key should not be pressed");
+ }
+
+ @Test
+ public void testMiddleClick() {
+ Div div = new Div("Middle click me");
+ container.add(div);
+
+ AtomicReference buttonPressed = new AtomicReference<>();
+ div.addClickListener(e -> buttonPressed.set(e.getButton()));
+
+ ComponentTester divTester = test(div);
+ divTester.middleClick();
+
+ Assertions.assertEquals(1, buttonPressed.get(),
+ "Middle button (1) should be pressed");
+ }
+
+ @Test
+ public void testRightClick() {
+ Span span = new Span("Right click me");
+ container.add(span);
+
+ AtomicReference
buttonPressed = new AtomicReference<>();
+ span.addClickListener(e -> buttonPressed.set(e.getButton()));
+
+ ComponentTester spanTester = test(span);
+ spanTester.rightClick();
+
+ Assertions.assertEquals(2, buttonPressed.get(),
+ "Right button (2) should be pressed");
+ }
+
+ @Test
+ public void testClick_componentDisabled_throwsException() {
+ Button button = new Button("Disabled button");
+ button.setEnabled(false);
+ container.add(button);
+
+ ComponentTester buttonTester = test(button);
+
+ Assertions.assertThrows(IllegalStateException.class,
+ buttonTester::click,
+ "Should throw exception when clicking disabled component");
+ }
+
+ @Test
+ public void testClick_componentNotVisible_throwsException() {
+ Div div = new Div("Hidden div");
+ div.setVisible(false);
+ container.add(div);
+
+ ComponentTester divTester = test(div);
+
+ Assertions.assertThrows(IllegalStateException.class, divTester::click,
+ "Should throw exception when clicking invisible component");
+ }
+
+ @Test
+ public void testClick_componentNotAttached_throwsException() {
+ Span span = new Span("Detached span");
+ // Not adding to container, so it's not attached
+
+ ComponentTester
spanTester = test(span);
+
+ Assertions.assertThrows(IllegalStateException.class, spanTester::click,
+ "Should throw exception when clicking detached component");
+ }
+
+ @Test
+ public void testClick_customComponent() {
+ // Custom component that doesn't implement ClickNotifier but handles
+ // click events directly
+ @Tag("custom-element")
+ class CustomComponent extends Component {
+ int clickCount = 0;
+
+ public CustomComponent() {
+ ComponentUtil.addListener(this, ClickEvent.class,
+ (ComponentEventListener) e -> {
+ clickCount++;
+ });
+ }
+ }
+
+ CustomComponent custom = new CustomComponent();
+ container.add(custom);
+
+ ComponentTester customTester = test(custom);
+ customTester.click();
+
+ Assertions.assertEquals(1, custom.clickCount,
+ "Click should work on custom components that handle click events");
+ }
+
+ @Test
+ public void testRightClick_withMetaKeys() {
+ Div div = new Div("Right click with meta");
+ container.add(div);
+
+ AtomicReference> clickEvent = new AtomicReference<>();
+ div.addClickListener(clickEvent::set);
+
+ ComponentTester divTester = test(div);
+ MetaKeys metaKeys = new MetaKeys().alt().meta();
+ divTester.rightClick(metaKeys);
+
+ Assertions.assertNotNull(clickEvent.get(),
+ "Click event should be fired");
+ Assertions.assertEquals(2, clickEvent.get().getButton(),
+ "Right button should be pressed");
+ Assertions.assertTrue(clickEvent.get().isAltKey(),
+ "Alt key should be pressed");
+ Assertions.assertTrue(clickEvent.get().isMetaKey(),
+ "Meta key should be pressed");
+ Assertions.assertFalse(clickEvent.get().isCtrlKey(),
+ "Ctrl key should not be pressed");
+ Assertions.assertFalse(clickEvent.get().isShiftKey(),
+ "Shift key should not be pressed");
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/ComponentQueryTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/ComponentQueryTest.java
new file mode 100644
index 000000000..fe033e149
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/ComponentQueryTest.java
@@ -0,0 +1,1444 @@
+/**
+ * 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 java.util.List;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.browserless.ComponentTesterTest.Span;
+import com.vaadin.browserless.ElementConditionsTest.TextComponent;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HasLabel;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.component.Text;
+import com.vaadin.flow.component.UI;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.textfield.IntegerField;
+import com.vaadin.flow.component.textfield.TextField;
+import com.vaadin.flow.component.textfield.TextFieldBase;
+import com.vaadin.flow.dom.Element;
+
+import static java.util.Arrays.asList;
+
+class ComponentQueryTest extends BrowserlessTest {
+
+ @Test
+ void find_invisibleComponents_noResults() {
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new Div().getElement(), new Div().getElement(),
+ new Div().getElement());
+ rootElement.getChildren().filter(el -> !el.isTextNode())
+ .forEach(el -> el.setVisible(false));
+
+ Assertions.assertTrue($(Div.class).all().isEmpty());
+ }
+
+ @Test
+ void first_exactMatch_getsComponent() {
+ Element root = getCurrentView().getElement();
+
+ TextField textField = new TextField();
+ root.appendChild(textField.getElement());
+ Button button = new Button();
+ root.appendChild(button.getElement());
+
+ ComponentQuery
textFieldQuery = $(TextField.class);
+ Assertions.assertSame(textField, textFieldQuery.first(),
+ "Expecting query to find TextField component, but got different instance");
+
+ ComponentQuery buttonQuery = $(Button.class);
+ Assertions.assertSame(button, buttonQuery.first(),
+ "Expecting query to find Button component, but got different instance");
+
+ }
+
+ @Test
+ void first_multipleMatching_getsFirstComponent() {
+ TextField first = new TextField();
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(first.getElement(),
+ new TextField().getElement(), new TextField().getElement(),
+ new TextField().getElement());
+
+ ComponentQuery query = $(TextField.class);
+ Assertions.assertSame(first, query.first(),
+ "Expecting query to find TextField component, but got different instance");
+ }
+
+ @Test
+ void first_noMatching_throws() {
+ ComponentQuery query = $(TextField.class);
+ Assertions.assertThrows(NoSuchElementException.class, query::first);
+ }
+
+ @Test
+ void last_exactMatch_getsComponent() {
+ Element root = getCurrentView().getElement();
+
+ TextField textField = new TextField();
+ root.appendChild(textField.getElement());
+ Button button = new Button();
+ root.appendChild(button.getElement());
+
+ ComponentQuery textFieldQuery = $(TextField.class);
+ Assertions.assertSame(textField, textFieldQuery.last(),
+ "Expecting query to find TextField component, but got different instance");
+
+ ComponentQuery buttonQuery = $(Button.class);
+ Assertions.assertSame(button, buttonQuery.last(),
+ "Expecting query to find Button component, but got different instance");
+
+ }
+
+ @Test
+ void last_multipleMatching_getsLastComponent() {
+ TextField last = new TextField();
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new TextField().getElement(),
+ new TextField().getElement(), new TextField().getElement(),
+ last.getElement());
+
+ ComponentQuery query = $(TextField.class);
+ Assertions.assertSame(last, query.last(),
+ "Expecting query to find TextField component, but got different instance");
+ }
+
+ @Test
+ void last_noMatching_throws() {
+ ComponentQuery query = $(TextField.class);
+ Assertions.assertThrows(NoSuchElementException.class, query::last);
+ }
+
+ @Test
+ void atIndex_exactMatch_getsComponent() {
+ Element root = getCurrentView().getElement();
+
+ TextField textField = new TextField();
+ root.appendChild(textField.getElement());
+ Button button = new Button();
+ root.appendChild(button.getElement());
+
+ ComponentQuery textFieldQuery = $(TextField.class);
+ Assertions.assertSame(textField, textFieldQuery.atIndex(1),
+ "Expecting query to find TextField component, but got different instance");
+
+ ComponentQuery buttonQuery = $(Button.class);
+ Assertions.assertSame(button, buttonQuery.atIndex(1),
+ "Expecting query to find Button component, but got different instance");
+
+ }
+
+ @Test
+ void atIndex_multipleMatching_getsFirstComponent() {
+ TextField last = new TextField();
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new TextField().getElement(),
+ new TextField().getElement(), new TextField().getElement(),
+ last.getElement());
+
+ ComponentQuery query = $(TextField.class);
+ Assertions.assertSame(last, query.atIndex(4),
+ "Expecting query to find TextField component, but got different instance");
+ }
+
+ @Test
+ void atIndex_negativeOrZeroIndex_throws() {
+ ComponentQuery query = $(TextField.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.atIndex(-10));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.atIndex(0));
+ }
+
+ @Test
+ void atIndex_outOfUpperBound_throws() {
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new TextField().getElement(),
+ new TextField().getElement(), new TextField().getElement());
+ ComponentQuery query = $(TextField.class);
+ Assertions.assertThrows(IndexOutOfBoundsException.class,
+ () -> query.atIndex(4));
+ Assertions.assertThrows(IndexOutOfBoundsException.class,
+ () -> query.atIndex(100));
+ }
+
+ @Test
+ void atIndex_noMatching_throws() {
+ ComponentQuery query = $(TextField.class);
+ Assertions.assertThrows(NoSuchElementException.class,
+ () -> query.atIndex(1));
+ }
+
+ @Test
+ void all_noMatching_getsEmptyList() {
+ ComponentQuery query = $(TextField.class);
+ List result = query.all();
+ Assertions.assertNotNull(result);
+ Assertions.assertTrue(result.isEmpty(),
+ "Expecting no results from search, but got " + result);
+ }
+
+ @Test
+ void all_matching_getsMatchingComponents() {
+
+ List expectedComponents = asList(new TextField(),
+ new TextField(), new TextField());
+ Element rootElement = getCurrentView().getElement();
+ expectedComponents
+ .forEach(text -> rootElement.appendChild(text.getElement()));
+
+ ComponentQuery query = $(TextField.class);
+ List result = query.all();
+ Assertions.assertNotNull(result);
+ Assertions.assertIterableEquals(expectedComponents, result);
+
+ Assertions.assertIterableEquals(
+ Collections
+ .singleton(getCurrentView().getElement().getChild(0)),
+ $(Text.class).all().stream().map(Component::getElement)
+ .collect(Collectors.toList()));
+ }
+
+ @Test
+ void allComponents_noMatching_getsEmptyList() {
+ ComponentQuery query = $(TextField.class);
+ List result = query.all();
+ Assertions.assertNotNull(result);
+ Assertions.assertEquals(0, result.size(),
+ "Expecting zero results from search, but got " + result.size());
+ }
+
+ @Test
+ void allComponents_matching_getsMatchingComponents() {
+
+ List expectedComponents = asList(new TextField(),
+ new TextField(), new TextField());
+ Element rootElement = getCurrentView().getElement();
+ expectedComponents
+ .forEach(text -> rootElement.appendChild(text.getElement()));
+
+ ComponentQuery query = $(TextField.class);
+ List result = query.all();
+ Assertions.assertIterableEquals(expectedComponents, result);
+
+ Assertions.assertIterableEquals(
+ Collections
+ .singleton(getCurrentView().getElement().getChild(0)),
+ $(Text.class).all().stream().map(Component::getElement)
+ .collect(Collectors.toList()));
+ }
+
+ @Test
+ void from_matchingChildren_getsNestedComponents() {
+ Div context = new Div();
+ TextField textField1 = new TextField();
+ TextField textField2 = new TextField();
+ TextField textField3 = new TextField();
+ TextField textField4 = new TextField();
+ TextField textField5 = new TextField();
+
+ context.add(new Div(textField1), textField2);
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(textField5.getElement(),
+ new Div(context, textField3).getElement(),
+ textField4.getElement());
+
+ List result = $(TextField.class).from(context).all();
+ Assertions.assertIterableEquals(List.of(textField1, textField2),
+ result);
+
+ result = $view(TextField.class).all();
+ Assertions.assertIterableEquals(List.of(textField5, textField1,
+ textField2, textField3, textField4), result);
+
+ }
+
+ @Test
+ void from_emptyContext_getsEmptyList() {
+ Div context = new Div();
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new TextField().getElement(),
+ new Div(context, new TextField()).getElement(),
+ new TextField().getElement());
+
+ List result = $(TextField.class).from(context).all();
+ Assertions.assertTrue(result.isEmpty());
+
+ // shorthand for from
+ result = $(TextField.class, context).all();
+ Assertions.assertTrue(result.isEmpty());
+
+ }
+
+ @Test
+ void from_noMatchingChildren_getsEmptyList() {
+ Div context = new Div();
+ context.add(new Div(new Button()), new Button());
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new Div(context, new TextField()).getElement(),
+ new TextField().getElement());
+
+ List result = $(TextField.class).from(context).all();
+ Assertions.assertTrue(result.isEmpty());
+
+ // shorthand for from
+ result = $(TextField.class, context).all();
+ Assertions.assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void from_withCriteria_getsComponentInContext() {
+ TextField notInViewTextField = new TextField();
+ notInViewTextField.setId("myId");
+ UI.getCurrent().getElement()
+ .appendChild(notInViewTextField.getElement());
+
+ TextField inViewTextField = new TextField();
+ inViewTextField.setId("myId");
+ Div context = new Div(inViewTextField);
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(context.getElement());
+
+ List result = $(TextField.class).from(context).all();
+ Assertions.assertIterableEquals(Collections.singleton(inViewTextField),
+ result);
+
+ TextField foundTextField = $(TextField.class).from(context).id("myId");
+ Assertions.assertSame(inViewTextField, foundTextField);
+
+ // shorthand for from
+ result = $(TextField.class, context).all();
+ Assertions.assertIterableEquals(Collections.singleton(inViewTextField),
+ result);
+
+ foundTextField = $(TextField.class, context).id("myId");
+ Assertions.assertSame(inViewTextField, foundTextField);
+ }
+
+ @Test
+ void id_matchingComponent_getsComponent() {
+ Element rootElement = getCurrentView().getElement();
+ List textFields = IntStream.rangeClosed(1, 5)
+ .mapToObj(idx -> {
+ TextField field = new TextField();
+ field.setId("field-" + idx);
+ return field;
+ }).peek(field -> rootElement.appendChild(field.getElement()))
+ .collect(Collectors.toList());
+
+ ComponentQuery query = $view(TextField.class);
+
+ textFields.forEach(field -> Assertions.assertSame(field,
+ query.id(field.getId().orElse(""))));
+ }
+
+ @Test
+ void id_noMatchingComponent_throws() {
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new TextField().getElement());
+ TextField textField = new TextField();
+ textField.setId("myId");
+ rootElement.appendChild(textField.getElement());
+
+ ComponentQuery query = $view(TextField.class);
+ Assertions.assertThrows(NoSuchElementException.class,
+ () -> query.id("test"));
+ }
+
+ @Test
+ void id_matchingDifferentComponentType_throws() {
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new TextField().getElement());
+ Button button = new Button();
+ button.setId("myId");
+ rootElement.appendChild(button.getElement());
+
+ ComponentQuery query = $view(TextField.class);
+ Assertions.assertThrows(NoSuchElementException.class,
+ () -> query.id("myId"));
+ }
+
+ @Test
+ void withId_matchingComponent_getsComponent() {
+ Element rootElement = getCurrentView().getElement();
+ List textFields = IntStream.rangeClosed(1, 5)
+ .mapToObj(idx -> {
+ TextField field = new TextField();
+ field.setId("field-" + idx);
+ return field;
+ }).peek(field -> rootElement.appendChild(field.getElement()))
+ .collect(Collectors.toList());
+
+ ComponentQuery query = $view(TextField.class);
+
+ for (TextField expected : textFields) {
+ List result = query.withId(expected.getId().orElse(""))
+ .all();
+ Assertions.assertIterableEquals(Collections.singleton(expected),
+ result);
+ }
+ }
+
+ @Test
+ void withId_noMatchingComponent_emptyList() {
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new TextField().getElement());
+ TextField textField = new TextField();
+ textField.setId("myId");
+ rootElement.appendChild(textField.getElement());
+
+ ComponentQuery query = $view(TextField.class);
+ Assertions.assertTrue(query.withId("wrongId").all().isEmpty());
+ }
+
+ @Test
+ void withId_matchingDifferentComponentType_emptyList() {
+ Element rootElement = getCurrentView().getElement();
+ rootElement.appendChild(new TextField().getElement());
+ Button button = new Button();
+ button.setId("myId");
+ rootElement.appendChild(button.getElement());
+
+ ComponentQuery query = $view(TextField.class);
+ Assertions.assertTrue(query.withId("myId").all().isEmpty());
+ }
+
+ @Test
+ void withPropertyValue_matchingValue_findsComponent() {
+ Element rootElement = getCurrentView().getElement();
+ TextField textField = new TextField();
+ String label = "field label";
+ textField.setLabel(label);
+ rootElement.appendChild(textField.getElement());
+ TextField textField2 = new TextField();
+ textField2.setLabel("Another label");
+ rootElement.appendChild(textField2.getElement());
+ rootElement.appendChild(new TextField().getElement());
+
+ Assertions.assertSame(textField, $(TextField.class)
+ .withPropertyValue(TextField::getLabel, label).first());
+ }
+
+ @Test
+ void withPropertyValue_expectedNull_findsComponent() {
+ Element rootElement = getCurrentView().getElement();
+ TextField textField = new TextField();
+ rootElement.appendChild(textField.getElement());
+ TextField textField2 = new TextField();
+ textField2.setLabel("Another label");
+ rootElement.appendChild(textField2.getElement());
+
+ Assertions.assertSame(textField, $(TextField.class)
+ .withPropertyValue(TextField::getLabel, null).first());
+ }
+
+ @Test
+ void withPropertyValue_noMatchingValue_doesNotFindComponent() {
+ Element rootElement = getCurrentView().getElement();
+ TextField textField = new TextField();
+ rootElement.appendChild(textField.getElement());
+ TextField textField2 = new TextField();
+ textField2.setLabel("Another label");
+ rootElement.appendChild(textField2.getElement());
+
+ Assertions.assertTrue($(TextField.class)
+ .withPropertyValue(TextField::getLabel, "The label").all()
+ .isEmpty());
+ }
+
+ @Test
+ void withValue_matchingValue_findsComponent() {
+ Element rootElement = getCurrentView().getElement();
+ TextField targetField = new TextField();
+ String targetValue = "expected value";
+ targetField.setValue(targetValue);
+ rootElement.appendChild(targetField.getElement());
+ TextField otherField = new TextField();
+ otherField.setValue("Another value");
+ int targetNumericValue = 33;
+ IntegerField numericField = new IntegerField();
+ numericField.setValue(targetNumericValue);
+
+ rootElement.appendChild(otherField.getElement());
+ rootElement.appendChild(numericField.getElement());
+ rootElement.appendChild(new TextField().getElement());
+
+ Assertions.assertSame(targetField,
+ $(Component.class).withValue(targetValue).first());
+ Assertions.assertSame(numericField,
+ $(Component.class).withValue(targetNumericValue).first());
+
+ }
+
+ @Test
+ void withValue_expectedNull_findsAllComponent() {
+ Element rootElement = getCurrentView().getElement();
+ TextField targetField = new TextField();
+ rootElement.appendChild(targetField.getElement());
+ TextField otherField = new TextField();
+ otherField.setValue("A value");
+ rootElement.appendChild(otherField.getElement());
+
+ Assertions.assertIterableEquals(List.of(targetField, otherField),
+ $(TextFieldBase.class).withValue(null).all());
+ }
+
+ @Test
+ void withValue_noMatchingValue_doesNotFindComponent() {
+ Element rootElement = getCurrentView().getElement();
+ TextField textField = new TextField();
+ rootElement.appendChild(textField.getElement());
+ TextField textField2 = new TextField();
+ textField2.setLabel("Another value");
+ rootElement.appendChild(textField2.getElement());
+
+ Assertions.assertTrue(
+ $(TextField.class).withValue("The value").all().isEmpty());
+
+ Assertions.assertTrue($(TextField.class).withValue(35).all().isEmpty());
+ }
+
+ @Test
+ void withCondition_predicateMatched_getsComponents() {
+ Div div1 = new Div();
+ div1.getElement().setProperty("numeric-prop", 4.5);
+ Div div2 = new Div(new Div(), new Div());
+ div2.getElement().setProperty("numeric-prop", 2.0);
+ Div div3 = new Div();
+ div3.getElement().setProperty("numeric-prop", 1.5);
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement());
+
+ List result = $(Div.class)
+ .withCondition(div -> div.getChildren().findAny().isPresent())
+ .all();
+ Assertions.assertIterableEquals(Collections.singleton(div2), result);
+
+ result = $(Div.class).withCondition(div -> {
+ double value = div.getElement().getProperty("numeric-prop", 0.0);
+ return value > 1 && value < 3;
+ }).all();
+ Assertions.assertIterableEquals(List.of(div2, div3), result);
+ }
+
+ @Test
+ void withCaption_exactMatch_getsCorrectComponent() {
+ ComponentWithLabel hasLabelCmp = new ComponentWithLabel();
+ hasLabelCmp.setLabel("has-label");
+
+ TestComponent propertyCmp = new TestComponent();
+ propertyCmp.getElement().setProperty("label", "property-label");
+
+ TestComponent noLabel = new TestComponent();
+
+ UI.getCurrent().getElement().appendChild(hasLabelCmp.getElement(),
+ propertyCmp.getElement(), noLabel.getElement());
+
+ Assertions.assertSame(hasLabelCmp,
+ $(TestComponent.class).withCaption("has-label").single());
+ Assertions.assertSame(propertyCmp,
+ $(TestComponent.class).withCaption("property-label").single());
+
+ Assertions.assertTrue(
+ $(TestComponent.class).withCaption("label").all().isEmpty());
+ }
+
+ @Test
+ void withCaption_null_getsAllComponent() {
+ ComponentWithLabel hasLabelCmp = new ComponentWithLabel();
+ hasLabelCmp.setLabel("has-label");
+
+ TestComponent propertyCmp = new TestComponent();
+ propertyCmp.getElement().setProperty("label", "property-label");
+
+ TestComponent noLabel = new TestComponent();
+
+ UI.getCurrent().getElement().appendChild(hasLabelCmp.getElement(),
+ propertyCmp.getElement(), noLabel.getElement());
+
+ Assertions.assertIterableEquals(
+ List.of(hasLabelCmp, propertyCmp, noLabel),
+ $(TestComponent.class).withCaption(null).all());
+ }
+
+ @Test
+ void withCaptionContaining_getsCorrectComponent() {
+ ComponentWithLabel hasLabelCmp = new ComponentWithLabel();
+ hasLabelCmp.setLabel("cmp-has-label");
+
+ TestComponent propertyCmp = new TestComponent();
+ propertyCmp.getElement().setProperty("label", "cmp-property-label");
+
+ TestComponent noLabel = new TestComponent();
+
+ UI.getCurrent().getElement().appendChild(hasLabelCmp.getElement(),
+ propertyCmp.getElement(), noLabel.getElement());
+
+ Assertions.assertIterableEquals(List.of(hasLabelCmp, propertyCmp),
+ $(TestComponent.class).withCaptionContaining("-lab").all());
+ Assertions.assertIterableEquals(List.of(hasLabelCmp, propertyCmp),
+ $(TestComponent.class).withCaptionContaining("-label").all());
+ Assertions.assertIterableEquals(List.of(hasLabelCmp, propertyCmp),
+ $(TestComponent.class).withCaptionContaining("cmp-").all());
+ Assertions.assertIterableEquals(
+ List.of(hasLabelCmp, propertyCmp, noLabel),
+ $(TestComponent.class).withCaptionContaining("").all());
+ Assertions.assertTrue($(TestComponent.class)
+ .withCaptionContaining("sometext").all().isEmpty());
+ }
+
+ @Test
+ void withCaptionContaining_null_throws() {
+ ComponentQuery
query = $(TestComponent.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withCaptionContaining(null));
+ }
+
+ @Test
+ void withText_exactMatch_getsCorrectComponent() {
+ TextComponent span1 = new TextComponent("sample text");
+ TextComponent span2 = new TextComponent("other text");
+ TextComponent span3 = new TextComponent(null);
+
+ UI.getCurrent().getElement().appendChild(span1.getElement(),
+ span2.getElement(), span3.getElement());
+
+ Assertions.assertSame(span1,
+ $(TextComponent.class).withText("sample text").single());
+ Assertions.assertSame(span2,
+ $(TextComponent.class).withText("other text").single());
+
+ Assertions.assertTrue(
+ $(TextComponent.class).withText("text").all().isEmpty());
+ Assertions.assertTrue(
+ $(TextComponent.class).withText("SAMPLE TEXT").all().isEmpty());
+ Assertions.assertTrue(
+ $(TextComponent.class).withText("other TEXT").all().isEmpty());
+ }
+
+ @Test
+ void withTextContaining_getsCorrectComponent() {
+ TextComponent span1 = new TextComponent(
+ "this is sample text for first span");
+ TextComponent span2 = new TextComponent(
+ "this is other text second span");
+ TextComponent span3 = new TextComponent(null);
+
+ UI.getCurrent().getElement().appendChild(span1.getElement(),
+ span2.getElement(), span3.getElement());
+
+ Assertions.assertIterableEquals(List.of(span1, span2),
+ $(TextComponent.class).withTextContaining("text").all());
+ Assertions.assertIterableEquals(List.of(span1, span2),
+ $(TextComponent.class).withTextContaining("span").all());
+ Assertions.assertIterableEquals(List.of(span1, span2),
+ $(TextComponent.class).withTextContaining("this").all());
+ Assertions.assertIterableEquals(List.of(span1, span2, span3),
+ $(TextComponent.class).withTextContaining("").all());
+ Assertions.assertTrue($(TextComponent.class)
+ .withTextContaining("textual").all().isEmpty());
+ }
+
+ @Test
+ void withTextContaining_null_throws() {
+ ComponentQuery query = $(Span.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withTextContaining(null));
+ }
+
+ @Test
+ void withResultsSize_zeroMatchingResultsSize_emptyResult() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ Assertions.assertTrue(
+ $(TestComponent.class).withResultsSize(0).all().isEmpty());
+ }
+
+ @Test
+ void withResultsSize_matchingResultsSize_getsComponents() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ List result = $(Div.class).withResultsSize(4).all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3, div4),
+ result);
+ }
+
+ @Test
+ void withResultsSize_notMatchingResultsSize_throws() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ IntStream.rangeClosed(0, 10).filter(i -> i != 4).forEach(i -> {
+ ComponentQuery
query = $(Div.class).withResultsSize(i);
+ Assertions.assertThrows(AssertionError.class, query::all);
+ });
+ }
+
+ @Test
+ void withResultsSize_negative_throws() {
+ ComponentQuery
query = $(Div.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withResultsSize(-1));
+ }
+
+ @Test
+ void withResultsSize_afterWithMaxResults_overwritesRange() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ List
result = $(Div.class).withMaxResults(2).withResultsSize(4)
+ .all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3, div4),
+ result);
+ }
+
+ @Test
+ void withResultsSize_afterWithMinResults_overwritesRange() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ List
result = $(Div.class).withMinResults(10).withResultsSize(4)
+ .all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3, div4),
+ result);
+ }
+
+ @Test
+ void withResultsSize_afterWithResultSizeRange_overwritesRange() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ List
result = $(Div.class).withResultsSize(2, 3).withResultsSize(4)
+ .all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3, div4),
+ result);
+ }
+
+ @Test
+ void withMaxResults_resultsSizeWithinUpperBound_getsComponents() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ List
result = $(Div.class).withMaxResults(10).all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3, div4),
+ result);
+ result = $(Div.class).withMaxResults(4).all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3, div4),
+ result);
+ }
+
+ @Test
+ void withMaxResults_resultsSizeExceededUpperBound_throws() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ IntStream.rangeClosed(1, 3).forEach(count -> {
+ ComponentQuery
query = $(Div.class).withMaxResults(count);
+ Assertions.assertThrows(AssertionError.class, query::all);
+ });
+ }
+
+ @Test
+ void withMaxResults_negative_throws() {
+ ComponentQuery
query = $(Div.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withMaxResults(-1));
+ }
+
+ @Test
+ void withMaxResults_lowerThanMin_throws() {
+ ComponentQuery
query = $(Div.class).withMinResults(4);
+ query.withMaxResults(4); // same as min is OK, must not throw
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withMaxResults(2));
+ }
+
+ @Test
+ void withMaxResults_afterResultsSize_overwritesRange() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+ List
result = $(Div.class).withResultsSize(1).withMaxResults(4)
+ .all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3, div4),
+ result);
+ }
+
+ @Test
+ void withMinResults_resultsSizeWithinLowerBound_getsComponents() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ IntStream.rangeClosed(0, 4).forEach(count -> {
+ List
result = $(Div.class).withMinResults(count).all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3, div4),
+ result);
+ });
+ }
+
+ @Test
+ void withMinResults_resultsSizeLessThanLowerBound_throws() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement());
+
+ IntStream.rangeClosed(3, 10).forEach(count -> {
+ ComponentQuery
query = $(Div.class).withMinResults(count);
+ Assertions.assertThrows(AssertionError.class, query::all);
+ });
+ }
+
+ @Test
+ void withMinResults_negative_throws() {
+ ComponentQuery
query = $(Div.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withMinResults(-1));
+ }
+
+ @Test
+ void withMinResults_greaterThanMax_throws() {
+ ComponentQuery
query = $(Div.class).withMaxResults(2);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withMinResults(10));
+ }
+
+ @Test
+ void withMinResults_afterResultsSize_overwritesLowerRange() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement());
+
+ List
result = $(Div.class).withResultsSize(4).withMinResults(1)
+ .all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3), result);
+ }
+
+ @Test
+ void withResults_resultsSizeOutOfBounds_throws() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ ComponentQuery
query = $(Div.class).withResultsSize(5, 10);
+ Assertions.assertThrows(AssertionError.class, query::all);
+
+ query = $(Div.class).withResultsSize(1, 3);
+ Assertions.assertThrows(AssertionError.class, query::all);
+
+ }
+
+ @Test
+ void withResultsSize_minNegative_throws() {
+ ComponentQuery
query = $(Div.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withResultsSize(-1, 10));
+ }
+
+ @Test
+ void withResultsSize_maxNegative_throws() {
+ ComponentQuery
query = $(Div.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withResultsSize(2, -1));
+ }
+
+ @Test
+ void withResultsSize_maxLowerThanMin_throws() {
+ ComponentQuery
query = $(Div.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withResultsSize(5, 2));
+ }
+
+ @Test
+ void withResultsSize_afterResultsSize_overwritesRange() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+ List
result = $(Div.class).withResultsSize(5).withResultsSize(2, 5)
+ .all();
+ Assertions.assertIterableEquals(List.of(div1, div2, div3, div4),
+ result);
+ }
+
+ @Test
+ void thenOnFirst_chainedQuery_getsNestedComponents() {
+ TextField deepNested = new TextField();
+ Div nestedDiv = new Div(deepNested);
+ nestedDiv.setId("nestedDiv");
+ TextField nested1 = new TextField();
+ TextField nested2 = new TextField();
+ Div firstMatch = new Div(new Div(nestedDiv), nested1, new Div(nested2));
+ firstMatch.setId("myId");
+ UI.getCurrent().getElement().appendChild(
+ new Div(firstMatch).getElement(), new TextField().getElement());
+
+ List
result = $(Div.class).withId("myId")
+ .thenOnFirst(TextField.class).all();
+ Assertions.assertIterableEquals(List.of(deepNested, nested1, nested2),
+ result);
+
+ result = $(Div.class).withId("myId").thenOnFirst(Div.class)
+ .withId("nestedDiv").thenOnFirst(TextField.class).all();
+ Assertions.assertIterableEquals(List.of(deepNested), result);
+ }
+
+ @Test
+ void thenOnFirst_firstNotFound_throws() {
+ Div div = new Div(new TextField());
+ div.setVisible(false);
+ UI.getCurrent().getElement().appendChild(div.getElement());
+
+ ComponentQuery query = $(Div.class);
+ Assertions.assertThrows(NoSuchElementException.class,
+ () -> query.thenOnFirst(TextField.class));
+ }
+
+ @Test
+ void thenOn_chainedQuery_getsNestedComponents() {
+ TextField nested = new TextField();
+ UI.getCurrent().getElement().appendChild(
+ new Div(new TextField()).getElement(),
+ new Div(new TextField()).getElement(),
+ new Div(nested).getElement(),
+ new Div(new TextField()).getElement(),
+ new Div(new TextField()).getElement());
+
+ List
result = $(Div.class).thenOn(3, TextField.class).all();
+ Assertions.assertIterableEquals(List.of(nested), result);
+ }
+
+ @Test
+ void withTheme_getsCorrectComponent() {
+ Span target = new Span();
+ target.getElement().getThemeList().add("my-theme");
+ UI.getCurrent().getElement().appendChild(new Span().getElement(),
+ target.getElement(), new Span().getElement());
+
+ Assertions.assertEquals(target,
+ $(Span.class).withTheme("my-theme").single());
+ }
+
+ @Test
+ void withThemeMultipleThemes_getsCorrectComponent() {
+ Span target = new Span();
+ target.getElement().getThemeList().add("my-theme");
+ target.getElement().getThemeList().add("custom-theme");
+
+ final Span first = new Span();
+ first.getElement().getThemeList().add("my-theme");
+ final Span last = new Span();
+ last.getElement().getThemeList().add("my-theme");
+
+ UI.getCurrent().getElement().appendChild(first.getElement(),
+ target.getElement(), last.getElement());
+
+ Assertions.assertEquals(target, $(Span.class).withTheme("my-theme")
+ .withTheme("custom-theme").single());
+ }
+
+ @Test
+ void withoutTheme_getsCorrectComponent() {
+ Span target = new Span();
+ target.getElement().getThemeList().add("my-theme");
+
+ final Span first = new Span();
+ first.getElement().getThemeList().add("custom-theme");
+ final Span last = new Span();
+ last.getElement().getThemeList().add("custom-theme");
+
+ UI.getCurrent().getElement().appendChild(first.getElement(),
+ target.getElement(), last.getElement());
+
+ Assertions.assertEquals(target,
+ $(Span.class).withoutTheme("custom-theme").single());
+ }
+
+ @Test
+ void withoutThemeMultipleThemes_getsCorrectComponent() {
+ Span target = new Span();
+ target.getElement().getThemeList().add("selection-theme");
+
+ final Span first = new Span();
+ first.getElement().getThemeList().add("selection-theme");
+ first.getElement().getThemeList().add("my-theme");
+ final Span last = new Span();
+ last.getElement().getThemeList().add("selection-theme");
+ last.getElement().getThemeList().add("custom-theme");
+
+ UI.getCurrent().getElement().appendChild(first.getElement(),
+ target.getElement(), last.getElement());
+
+ Assertions.assertEquals(target, $(Span.class).withoutTheme("my-theme")
+ .withoutTheme("custom-theme").single());
+ }
+
+ @Test
+ void withClass_singleClassName_getsComponents() {
+ Div div1 = new Div();
+ div1.setClassName("test-class");
+ Div div2 = new Div();
+ div2.addClassName("test-class");
+ div2.addClassName("other-class");
+ Div div3 = new Div();
+ div3.addClassName("other-class");
+ Div div4 = new Div();
+ div4.setClassName("different-class");
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ new Div().getElement(), div2.getElement(), div3.getElement(),
+ div4.getElement());
+
+ List result = $(Div.class).withClassName("test-class").all();
+ Assertions.assertIterableEquals(List.of(div1, div2), result);
+
+ result = $(Div.class).withClassName("other-class").all();
+ Assertions.assertIterableEquals(List.of(div2, div3), result);
+
+ result = $(Div.class).withClassName("different-class").all();
+ Assertions.assertIterableEquals(List.of(div4), result);
+ }
+
+ @Test
+ void withClass_multipleClassNames_getsComponents() {
+ Div div1 = new Div();
+ div1.setClassName("test-class");
+ Div div2 = new Div();
+ div2.addClassName("test-class");
+ div2.addClassName("other-class");
+ Div div3 = new Div();
+ div3.addClassName("other-class");
+ Div div4 = new Div();
+ div4.setClassName("different-class");
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ new Div().getElement(), div2.getElement(), div3.getElement(),
+ div4.getElement());
+
+ List
result = $(Div.class)
+ .withClassName("test-class", "other-class").all();
+ Assertions.assertIterableEquals(List.of(div2), result);
+
+ result = $(Div.class).withClassName("test-class")
+ .withClassName("other-class").all();
+ Assertions.assertIterableEquals(List.of(div2), result);
+ }
+
+ @Test
+ void withClass_multipleSpaceSeparatedClassNames_getsComponents() {
+ Div div1 = new Div();
+ div1.setClassName("test-class");
+ Div div2 = new Div();
+ div2.addClassName("test-class");
+ div2.addClassName("other-class");
+ Div div3 = new Div();
+ div3.addClassName("other-class");
+ Div div4 = new Div();
+ div4.setClassName("different-class");
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ new Div().getElement(), div2.getElement(), div3.getElement(),
+ div4.getElement());
+
+ List
result = $(Div.class).withClassName("test-class other-class")
+ .all();
+ Assertions.assertIterableEquals(List.of(div2), result);
+ // order doesn't matter
+ result = $(Div.class).withClassName("other-class test-class").all();
+ Assertions.assertIterableEquals(List.of(div2), result);
+ }
+
+ @Test
+ void withClass_notAllClassApplied_doesNotFindComponents() {
+ Div div1 = new Div();
+ div1.setClassName("test-class");
+ Div div2 = new Div();
+ div2.addClassName("test-class");
+ div2.addClassName("other-class");
+ Div div3 = new Div();
+ div3.addClassName("other-class");
+ Div div4 = new Div();
+ div4.setClassName("different-class");
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ new Div().getElement(), div2.getElement(), div3.getElement(),
+ div4.getElement());
+
+ List
result = $(Div.class)
+ .withClassName("test-class", "different-class").all();
+ Assertions.assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void withClass_nullClassNames_throws() {
+ ComponentQuery
query = $(Div.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withClassName(null));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withClassName("c1", (String) null));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withClassName("c1", "c2", null, "c3"));
+ }
+
+ @Test
+ void withoutClass_singleClassName_getsComponents() {
+ Div div1 = new Div();
+ div1.setClassName("test-class");
+ Div div2 = new Div();
+ div2.addClassName("test-class");
+ div2.addClassName("other-class");
+ Div div3 = new Div();
+ div3.addClassName("other-class");
+ Div div4 = new Div();
+ div4.setClassName("different-class");
+ Div divWithotClasses = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ divWithotClasses.getElement(), div2.getElement(),
+ div3.getElement(), div4.getElement());
+
+ List
result = $(Div.class).withoutClassName("test-class").all();
+ Assertions.assertIterableEquals(List.of(divWithotClasses, div3, div4),
+ result);
+
+ result = $(Div.class).withoutClassName("other-class").all();
+ Assertions.assertIterableEquals(List.of(div1, divWithotClasses, div4),
+ result);
+
+ result = $(Div.class).withoutClassName("different-class").all();
+ Assertions.assertIterableEquals(
+ List.of(div1, divWithotClasses, div2, div3), result);
+ }
+
+ @Test
+ void withoutClass_multipleClassNames_getsComponents() {
+ Div div1 = new Div();
+ div1.setClassName("test-class");
+ Div div2 = new Div();
+ div2.addClassName("test-class");
+ div2.addClassName("other-class");
+ Div div3 = new Div();
+ div3.addClassName("other-class");
+ Div div4 = new Div();
+ div4.setClassName("different-class");
+ Div divWithoutClasses = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ divWithoutClasses.getElement(), div2.getElement(),
+ div3.getElement(), div4.getElement());
+
+ List
result = $(Div.class)
+ .withoutClassName("test-class", "other-class").all();
+ Assertions.assertIterableEquals(List.of(divWithoutClasses, div4),
+ result);
+
+ result = $(Div.class).withoutClassName("test-class")
+ .withoutClassName("other-class").all();
+ Assertions.assertIterableEquals(List.of(divWithoutClasses, div4),
+ result);
+ }
+
+ @Test
+ void withoutClass_multipleSpaceSeparatedClassNames_getsComponents() {
+ Div div1 = new Div();
+ div1.setClassName("test-class");
+ Div div2 = new Div();
+ div2.addClassName("test-class");
+ div2.addClassName("other-class");
+ Div div3 = new Div();
+ div3.addClassName("other-class");
+ Div div4 = new Div();
+ div4.setClassName("different-class");
+ Div divWithoutClasses = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ divWithoutClasses.getElement(), div2.getElement(),
+ div3.getElement(), div4.getElement());
+
+ List
result = $(Div.class)
+ .withoutClassName("test-class other-class").all();
+ Assertions.assertIterableEquals(List.of(divWithoutClasses, div4),
+ result);
+ result = $(Div.class).withoutClassName("other-class test-class").all();
+ Assertions.assertIterableEquals(List.of(divWithoutClasses, div4),
+ result);
+
+ }
+
+ @Test
+ void withoutClass_allClassApplied_doesNotFindComponents() {
+ Div div1 = new Div();
+ div1.setClassName("test-class");
+ Div div2 = new Div();
+ div2.addClassName("test-class");
+ div2.addClassName("other-class");
+ Div div3 = new Div();
+ div3.addClassName("other-class");
+ Div div4 = new Div();
+ div4.setClassName("different-class");
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ List
result = $(Div.class).withoutClassName("test-class",
+ "other-class", "different-class").all();
+ Assertions.assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void withoutClass_nullClassNames_throws() {
+ ComponentQuery
query = $(Div.class);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withoutClassName(null));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withoutClassName("c1", (String) null));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> query.withoutClassName("c1", "c2", null, "c3"));
+ }
+
+ @Test
+ void withAttribute_present_getsCorrectComponents() {
+ Span target = new Span();
+ target.getElement().setAttribute("my-attr", "value");
+ Span other = new Span();
+ other.getElement().setAttribute("other-attr", "value");
+ UI.getCurrent().getElement().appendChild(new Span().getElement(),
+ target.getElement(), new Span().getElement(),
+ other.getElement());
+
+ Assertions.assertSame(target,
+ $(Span.class).withAttribute("my-attr").single());
+ Assertions.assertSame(other,
+ $(Span.class).withAttribute("other-attr").single());
+ Assertions.assertTrue(
+ $(Span.class).withAttribute("nope").all().isEmpty());
+
+ }
+
+ @Test
+ void withAttribute_value_getsCorrectComponents() {
+ Span target = new Span();
+ target.getElement().setAttribute("my-attr", "value");
+ Span other = new Span();
+ other.getElement().setAttribute("my-attr", "something else");
+ UI.getCurrent().getElement().appendChild(new Span().getElement(),
+ target.getElement(), new Span().getElement(),
+ other.getElement());
+
+ Assertions.assertSame(target,
+ $(Span.class).withAttribute("my-attr", "value").single());
+ Assertions.assertSame(other, $(Span.class)
+ .withAttribute("my-attr", "something else").single());
+ Assertions.assertTrue(
+ $(Span.class).withAttribute("my-attr", "nope").all().isEmpty());
+ }
+
+ @Test
+ void withAttribute_multipleAttributes_getsCorrectComponents() {
+ Span target = new Span();
+ target.getElement().setAttribute("role", "tooltip");
+ target.getElement().setAttribute("aria-label", "some text");
+ Span other = new Span();
+ other.getElement().setAttribute("role", "something");
+ other.getElement().setAttribute("aria-label", "some other text");
+
+ UI.getCurrent().getElement().appendChild(new Span().getElement(),
+ target.getElement(), new Span().getElement(),
+ other.getElement());
+
+ Assertions.assertSame(target, $(Span.class).withAttribute("aria-label")
+ .withAttribute("role", "tooltip").single());
+ }
+
+ @Test
+ void withoutAttribute_absent_getsCorrectComponents() {
+ Span target = new Span();
+ target.getElement().setAttribute("my-attr", "value");
+ Span other = new Span();
+ other.getElement().setAttribute("other-attr", "value");
+ Span noAttributes = new Span();
+ UI.getCurrent().getElement().appendChild(target.getElement(),
+ noAttributes.getElement(), other.getElement());
+
+ Assertions.assertIterableEquals(List.of(noAttributes, other),
+ $(Span.class).withoutAttribute("my-attr").all());
+ Assertions.assertIterableEquals(List.of(target, noAttributes),
+ $(Span.class).withoutAttribute("other-attr").all());
+
+ Assertions.assertIterableEquals(List.of(target, noAttributes, other),
+ $(Span.class).withoutAttribute("role").all());
+ }
+
+ @Test
+ void withoutAttribute_value_getsCorrectComponents() {
+ Span target = new Span();
+ target.getElement().setAttribute("my-attr", "value");
+ Span other = new Span();
+ other.getElement().setAttribute("my-attr", "something else");
+ Span noAttributes = new Span();
+ UI.getCurrent().getElement().appendChild(target.getElement(),
+ noAttributes.getElement(), other.getElement());
+
+ Assertions.assertIterableEquals(List.of(noAttributes, other),
+ $(Span.class).withoutAttribute("my-attr", "value").all());
+ Assertions.assertIterableEquals(List.of(target, noAttributes),
+ $(Span.class).withoutAttribute("my-attr", "something else")
+ .all());
+ Assertions.assertIterableEquals(List.of(target, noAttributes, other),
+ $(Span.class).withoutAttribute("my-attr", "nope").all());
+ }
+
+ @Test
+ void withoutAttribute_multipleAttributes_getsCorrectComponents() {
+ Span span1 = new Span();
+ span1.getElement().setAttribute("role", "tooltip");
+ Span span2 = new Span();
+ span2.getElement().setAttribute("role", "something");
+ span2.getElement().setAttribute("aria-label", "some other text");
+ Span target = new Span();
+ target.getElement().setAttribute("role", "something");
+
+ UI.getCurrent().getElement().appendChild(span1.getElement(),
+ target.getElement(), span2.getElement());
+
+ Assertions.assertSame(target,
+ $(Span.class).withoutAttribute("aria-label")
+ .withoutAttribute("role", "tooltip").single());
+ }
+
+ @Test
+ void exists_matchingComponents_true() {
+ Element root = getCurrentView().getElement();
+
+ root.appendChild(new TextField().getElement());
+ root.appendChild(new TextField().getElement());
+ root.appendChild(new TextField().getElement());
+ root.appendChild(new TextField().getElement());
+ root.appendChild(new Button().getElement());
+
+ Assertions.assertTrue($(TextField.class).exists(),
+ "Expecting components to be found, but exists is false");
+
+ Assertions.assertTrue($(Button.class).exists(),
+ "Expecting components to be found, but exists is false");
+ }
+
+ @Test
+ void exists_noMatching_false() {
+ ComponentQuery
query = $(TextField.class);
+ Assertions.assertFalse(query.exists(),
+ "Expecting no components to be found, but exists is true");
+ }
+
+ @Test
+ void single_matchingExactlyOne_getsComponent() {
+ Span span = new Span();
+ Div div1 = new Div();
+ Div target = new Div();
+ target.setClassName("my-test");
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ target.getElement(), span.getElement(), div3.getElement(),
+ div4.getElement());
+
+ Assertions.assertSame(span, $(Span.class).single());
+ Assertions.assertSame(target,
+ $(Div.class).withClassName("my-test").single());
+ }
+
+ @Test
+ void single_noMatching_throws() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ ComponentQuery queryNonExistent = $(Span.class);
+ Assertions.assertThrows(NoSuchElementException.class,
+ queryNonExistent::single);
+
+ ComponentQuery query = $(Div.class).withClassName("my-test");
+ Assertions.assertThrows(NoSuchElementException.class, query::single);
+ }
+
+ @Test
+ void single_matchingMultipleComponents_throws() {
+ Div div1 = new Div();
+ Div div2 = new Div();
+ Div div3 = new Div();
+ Div div4 = new Div();
+ UI.getCurrent().getElement().appendChild(div1.getElement(),
+ div2.getElement(), div3.getElement(), div4.getElement());
+
+ ComponentQuery
query = $(Div.class);
+ Assertions.assertThrows(NoSuchElementException.class, query::single);
+ }
+
+ @Tag("span")
+ private static class ComponentWithLabel extends TestComponent
+ implements HasLabel {
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/ComponentTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/ComponentTesterTest.java
new file mode 100644
index 000000000..8e9a0fb40
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/ComponentTesterTest.java
@@ -0,0 +1,247 @@
+/**
+ * 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.List;
+import java.util.Optional;
+
+import com.example.base.WelcomeView;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HasComponents;
+import com.vaadin.flow.component.HasElement;
+import com.vaadin.flow.component.HasText;
+import com.vaadin.flow.component.Tag;
+
+@ViewPackages(packages = "com.example")
+public class ComponentTesterTest extends BrowserlessTest {
+
+ private WelcomeView home;
+
+ @BeforeEach
+ public void initHome() {
+ home = getHome();
+ }
+
+ @Test
+ public void canGetWrapperForView_viewIsUsable() {
+ final ComponentTester
home_ = test(home);
+ Assertions.assertTrue(home_.isUsable(),
+ "Home should be visible and interactable");
+ }
+
+ @Test
+ public void componentIsDisabled_isUsableReturnsFalse() {
+ home.getElement().setEnabled(false);
+
+ final ComponentTester home_ = test(home);
+ Assertions.assertFalse(home_.isUsable(),
+ "Home should be visible but not interactable");
+ }
+
+ @Test
+ public void componentIsHidden_isUsableReturnsFalse() {
+ home.setVisible(false);
+
+ final ComponentTester home_ = test(home);
+ Assertions.assertFalse(home_.isUsable(),
+ "Home should not be interactable when component is not visible");
+ }
+
+ @Test
+ public void componentModality_componentIsUsableReturnsCorrectly() {
+ final ComponentTester home_ = test(home);
+
+ final Span span = new Span();
+ home.add(span);
+ final ComponentTester span_ = test(span);
+
+ Assertions.assertTrue(span_.isUsable(),
+ "Span should be attached to the ui");
+
+ span_.setModal(true);
+
+ Assertions.assertTrue(span_.isUsable(),
+ "Span should interactable when it is modal");
+ Assertions.assertFalse(home_.isUsable(),
+ "Home should not be interactable when Span is modal");
+
+ span_.setModal(false);
+
+ Assertions.assertTrue(home_.isUsable(),
+ "Home should be interactable when Span is not modal");
+ }
+
+ @Test
+ public void componentModality_modalityDropsOnComponentRemoval() {
+ final ComponentTester home_ = test(home);
+
+ final Span span = new Span();
+ home.add(span);
+ final ComponentTester span_ = test(span);
+
+ Assertions.assertTrue(span_.isUsable(),
+ "Span should be attached to the ui");
+
+ span_.setModal(true);
+
+ Assertions.assertTrue(span_.isUsable(),
+ "Span should be interactable when it is modal");
+ Assertions.assertFalse(home_.isUsable(),
+ "Home should not be interactable when Span is modal");
+
+ home.remove(span);
+
+ Assertions.assertTrue(home_.isUsable(),
+ "Home should be interactable when Span is removed");
+ }
+
+ @Test
+ public void parentNotVisible_childIsNotInteractable() {
+ final Span span = new Span();
+ home.add(span);
+ final ComponentTester span_ = test(span);
+
+ Assertions.assertTrue(span_.isUsable(),
+ "Span should be attached to the ui");
+
+ home.setVisible(false);
+
+ Assertions.assertFalse(span_.isUsable(),
+ "Span should not be interactable when parent is hidden");
+ }
+
+ @Test
+ public void nonAttachedComponent_isNotInteractable() {
+ Span span = new Span();
+
+ ComponentTester span_ = test(span);
+
+ Assertions.assertFalse(span_.isUsable(),
+ "Span is not attached so it is not usable.");
+ }
+
+ @Test
+ void findByQuery_matchingComponent_getsComponent() {
+ Span one = new Span("One");
+ Span two = new Span("Two");
+ Div container = new Div(new Div(new Div(one)), new Div(two), new Div());
+
+ ComponentTester wrapper_ = test(container);
+
+ Optional
result = wrapper_.findByQuery(Span.class,
+ query -> query.withText("One"));
+ Assertions.assertTrue(result.isPresent());
+ Assertions.assertSame(one, result.get());
+
+ result = wrapper_.findByQuery(Span.class,
+ query -> query.withText("Two"));
+ Assertions.assertTrue(result.isPresent());
+ Assertions.assertSame(two, result.get());
+ }
+
+ @Test
+ void findByQuery_notMatchingComponent_empty() {
+ Span one = new Span("One");
+ Span two = new Span("Two");
+ Div container = new Div(new Div(new Div(one)), new Div(two), new Div());
+
+ ComponentTester wrapper_ = test(container);
+
+ Optional
result = wrapper_.findByQuery(Span.class,
+ query -> query.withText("Three"));
+ Assertions.assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void findByQuery_multipleMatchingComponents_throws() {
+ Span one = new Span("Span One");
+ Span two = new Span("Span Two");
+ Div container = new Div(new Div(new Div(one)), new Div(two), new Div());
+
+ ComponentTester wrapper_ = test(container);
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> wrapper_.findByQuery(Span.class,
+ query -> query.withTextContaining("Span")));
+ }
+
+ @Test
+ void findAllByQuery_matchingComponent_getsComponents() {
+ Span one = new Span("Span One");
+ Span two = new Span("Span Two");
+ Span three = new Span("Span Two bis");
+ Div container = new Div(new Div(new Div(one)), new Div(two),
+ new Div(three));
+
+ ComponentTester
wrapper_ = test(container);
+
+ List
result = wrapper_.findAllByQuery(Span.class,
+ query -> query.withTextContaining("One"));
+ Assertions.assertIterableEquals(List.of(one), result);
+
+ result = wrapper_.findAllByQuery(Span.class,
+ query -> query.withTextContaining("Two"));
+ Assertions.assertIterableEquals(List.of(two, three), result);
+
+ result = wrapper_.findAllByQuery(Span.class,
+ query -> query.withTextContaining("Span"));
+ Assertions.assertIterableEquals(List.of(one, two, three), result);
+ }
+
+ @Test
+ void findAllByQuery_notMatchingComponent_empty() {
+ Span one = new Span("Span One");
+ Span two = new Span("Span Two");
+ Span three = new Span("Span Two bis");
+ Div container = new Div(new Div(new Div(one)), new Div(two),
+ new Div(three));
+
+ ComponentTester wrapper_ = test(container);
+
+ List
result = wrapper_.findAllByQuery(Span.class,
+ query -> query.withTextContaining("Three"));
+ Assertions.assertTrue(result.isEmpty());
+ }
+
+ private WelcomeView getHome() {
+ final HasElement view = getCurrentView();
+ Assertions.assertTrue(view instanceof WelcomeView,
+ "Home should be navigated to by default");
+ return (WelcomeView) view;
+ }
+
+ @Tag("span")
+ public static class Span extends Component implements HasText {
+ public Span() {
+ }
+
+ public Span(String text) {
+ setText(text);
+ }
+ }
+
+ @Tag("div")
+ public static class Div extends Component implements HasComponents {
+ public Div(Component... components) {
+ add(components);
+ }
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/ElementConditionsTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/ElementConditionsTest.java
new file mode 100644
index 000000000..6d8c17b1a
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/ElementConditionsTest.java
@@ -0,0 +1,332 @@
+/**
+ * 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.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HasComponents;
+import com.vaadin.flow.component.HasText;
+import com.vaadin.flow.component.Html;
+import com.vaadin.flow.component.HtmlComponent;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.dom.Element;
+
+import static com.vaadin.browserless.ElementConditions.containsText;
+import static com.vaadin.browserless.ElementConditions.hasAttribute;
+import static com.vaadin.browserless.ElementConditions.hasNotAttribute;
+
+class ElementConditionsTest {
+
+ @Test
+ void containsText_null_throws() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> containsText(null));
+ }
+
+ @Test
+ void containsText_textNode_checksElementGetText() {
+ TextComponent component = new TextComponent("this is the content");
+ Assertions.assertTrue(containsText("").test(component));
+ Assertions.assertTrue(containsText("is").test(component));
+ Assertions.assertTrue(containsText("this").test(component));
+ Assertions.assertTrue(containsText("content").test(component));
+ Assertions.assertTrue(containsText(" is the ").test(component));
+ Assertions.assertTrue(
+ containsText("this is the content").test(component));
+
+ Assertions.assertFalse(containsText("some text").test(component));
+ Assertions.assertFalse(containsText("CONTENT").test(component));
+ }
+
+ @Test
+ void containsText_HasText_checksHasTextGetText() {
+ HasTextComponent component = new HasTextComponent(
+ "this is the content");
+ Assertions.assertTrue(containsText("").test(component));
+
+ Assertions.assertTrue(containsText("IS").test(component));
+ Assertions.assertTrue(containsText("THIS").test(component));
+ Assertions.assertTrue(containsText("CONTENT").test(component));
+ Assertions.assertTrue(containsText(" IS THE ").test(component));
+ Assertions.assertTrue(
+ containsText("THIS IS THE CONTENT").test(component));
+
+ Assertions.assertFalse(containsText("is").test(component));
+ Assertions.assertFalse(containsText("this").test(component));
+ Assertions.assertFalse(containsText("content").test(component));
+ Assertions.assertFalse(containsText(" is the ").test(component));
+ Assertions.assertFalse(
+ containsText("this is the content").test(component));
+ }
+
+ @Test
+ void containsText_Html_checksHasTextInInnerHtml() {
+ Html component = new Html("this is the content
");
+ Assertions.assertTrue(containsText("").test(component));
+
+ Assertions.assertTrue(containsText("").test(component));
+ Assertions.assertTrue(containsText("is").test(component));
+ Assertions.assertTrue(containsText("this").test(component));
+ Assertions.assertTrue(containsText("content").test(component));
+ Assertions.assertTrue(containsText(" is the ").test(component));
+ Assertions.assertTrue(
+ containsText("this is the content").test(component));
+
+ Assertions.assertFalse(containsText("some text").test(component));
+ Assertions.assertFalse(containsText("CONTENT").test(component));
+ }
+
+ @Test
+ void containsText_HtmlContainer_checksHasTextInInnerHtml() {
+ Article component = new Article(new TextComponent("this is"),
+ new NonTextComponent(" the "), new TextComponent("content"));
+ Assertions.assertTrue(containsText("").test(component));
+
+ Assertions.assertTrue(containsText("").test(component));
+ Assertions.assertTrue(containsText("is").test(component));
+ Assertions.assertTrue(containsText("this").test(component));
+ Assertions.assertTrue(containsText("content").test(component));
+ Assertions.assertTrue(containsText("is the").test(component));
+ Assertions.assertTrue(
+ containsText("this is the content").test(component));
+
+ Assertions.assertFalse(containsText("some text").test(component));
+ Assertions.assertFalse(containsText("CONTENT").test(component));
+ }
+
+ @Test
+ void containsText_nonTextNode_checksTextRecursively() {
+ NonTextComponent component = new NonTextComponent(
+ "this is the content");
+ Assertions.assertTrue(containsText("").test(component));
+ Assertions.assertTrue(containsText("is").test(component));
+ Assertions.assertTrue(containsText("this").test(component));
+ Assertions.assertTrue(containsText("content").test(component));
+ Assertions.assertTrue(containsText(" is the ").test(component));
+ Assertions.assertTrue(
+ containsText("this is the content").test(component));
+ Assertions.assertFalse(containsText("some text").test(component));
+ Assertions.assertFalse(containsText("CONTENT").test(component));
+ }
+
+ @Test
+ void containsText_nonTextNodeWithChildren_checksTextRecursively() {
+ NonTextComponent component = new NonTextComponent(
+ new NonTextComponent("this is"),
+ new TextComponent("the content"));
+ Assertions.assertTrue(containsText("").test(component));
+ Assertions.assertTrue(containsText("is").test(component));
+ Assertions.assertTrue(containsText("this").test(component));
+ Assertions.assertTrue(containsText("content").test(component));
+ Assertions.assertTrue(containsText("isthe").test(component));
+ Assertions
+ .assertTrue(containsText("this isthe content").test(component));
+ Assertions.assertFalse(containsText("some text").test(component));
+ Assertions.assertFalse(containsText("CONTENT").test(component));
+ }
+
+ @Test
+ void containsText_ignoreCase() {
+ TextComponent component = new HasTextComponent("this IS the CONTENT");
+ Assertions.assertTrue(containsText("", true).test(component));
+
+ Assertions.assertTrue(containsText("IS", true).test(component));
+ Assertions.assertTrue(containsText("THIS", true).test(component));
+ Assertions.assertTrue(containsText("CONTENT", true).test(component));
+ Assertions.assertTrue(containsText(" IS THE ", true).test(component));
+ Assertions.assertTrue(
+ containsText("THIS IS THE CONTENT", true).test(component));
+
+ Assertions.assertTrue(containsText("is", true).test(component));
+ Assertions.assertTrue(containsText("this", true).test(component));
+ Assertions.assertTrue(containsText("content", true).test(component));
+ Assertions.assertTrue(containsText(" is the ", true).test(component));
+ Assertions.assertTrue(
+ containsText("this is the content", true).test(component));
+
+ Assertions.assertTrue(
+ containsText("this IS the CONTENT", true).test(component));
+ Assertions.assertTrue(
+ containsText("THIS is THE content", true).test(component));
+
+ Assertions.assertTrue(containsText("THIS is THE content", true)
+ .test(new Html("this IS the CONTENT
")));
+
+ }
+
+ @Test
+ void hasAttribute_checksAttributePresence() {
+ TestComponent component = new TestComponent();
+ component.getElement().setAttribute("string-attribute", "primary");
+ component.getElement().setAttribute("empty-attribute", "");
+ component.getElement().setAttribute("true-attribute", true);
+ component.getElement().setAttribute("false-attribute", false);
+
+ Assertions.assertTrue(hasAttribute("string-attribute").test(component));
+ Assertions.assertTrue(hasAttribute("empty-attribute").test(component));
+ Assertions.assertTrue(hasAttribute("true-attribute").test(component));
+
+ Assertions.assertFalse(hasAttribute("false-attribute").test(component));
+
+ Assertions
+ .assertFalse(hasAttribute("not-set-attribute").test(component));
+
+ }
+
+ @Test
+ void hasAttribute_expectedValue_checksAttributeHasExactlyExpectedValue() {
+ TestComponent component = new TestComponent();
+ component.getElement().setAttribute("string-attribute", "primary");
+ component.getElement().setAttribute("empty-attribute", "");
+ component.getElement().setAttribute("true-attribute", true);
+ component.getElement().setAttribute("false-attribute", false);
+
+ Assertions.assertTrue(
+ hasAttribute("string-attribute", "primary").test(component));
+ Assertions.assertFalse(
+ hasAttribute("string-attribute", "PRIMARY").test(component));
+ Assertions.assertFalse(
+ hasAttribute("string-attribute", "").test(component));
+
+ Assertions.assertTrue(
+ hasAttribute("empty-attribute", "").test(component));
+
+ Assertions
+ .assertTrue(hasAttribute("true-attribute", "").test(component));
+ Assertions.assertFalse(
+ hasAttribute("true-attribute", "true").test(component));
+ Assertions.assertFalse(
+ hasAttribute("false-attribute", "").test(component));
+
+ Assertions.assertFalse(
+ hasAttribute("not-set-attribute", "").test(component));
+ Assertions.assertFalse(
+ hasAttribute("not-set-attribute", "test").test(component));
+ }
+
+ @Test
+ void hasAttribute_nullExpectedValue_throws() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> hasAttribute("attr", null));
+ }
+
+ @Test
+ void hasNotAttribute_checksAttributeAbsence() {
+ TestComponent component = new TestComponent();
+ component.getElement().setAttribute("string-attribute", "primary");
+ component.getElement().setAttribute("empty-attribute", "");
+ component.getElement().setAttribute("true-attribute", true);
+ component.getElement().setAttribute("false-attribute", false);
+
+ Assertions.assertFalse(
+ hasNotAttribute("string-attribute").test(component));
+ Assertions.assertFalse(
+ hasNotAttribute("empty-attribute").test(component));
+ Assertions
+ .assertFalse(hasNotAttribute("true-attribute").test(component));
+
+ Assertions
+ .assertTrue(hasNotAttribute("false-attribute").test(component));
+ Assertions.assertTrue(
+ hasNotAttribute("not-set-attribute").test(component));
+
+ }
+
+ @Test
+ void hasNotAttribute_expectedValue_checksAttributeValueIsDifferentFromGiven() {
+ TestComponent component = new TestComponent();
+ component.getElement().setAttribute("string-attribute", "primary");
+ component.getElement().setAttribute("empty-attribute", "");
+ component.getElement().setAttribute("true-attribute", true);
+ component.getElement().setAttribute("false-attribute", false);
+
+ Assertions.assertFalse(
+ hasNotAttribute("string-attribute", "primary").test(component));
+ Assertions.assertTrue(
+ hasNotAttribute("string-attribute", "PRIMARY").test(component));
+ Assertions.assertTrue(
+ hasNotAttribute("string-attribute", "").test(component));
+
+ Assertions.assertFalse(
+ hasNotAttribute("empty-attribute", "").test(component));
+
+ Assertions.assertFalse(
+ hasNotAttribute("true-attribute", "").test(component));
+ Assertions.assertTrue(
+ hasNotAttribute("true-attribute", "true").test(component));
+ Assertions.assertTrue(
+ hasNotAttribute("false-attribute", "").test(component));
+
+ Assertions.assertTrue(
+ hasNotAttribute("not-set-attribute", "").test(component));
+ Assertions.assertTrue(
+ hasNotAttribute("not-set-attribute", "test").test(component));
+ }
+
+ @Test
+ void hasNotAttribute_nullExpectedValue_throws() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> hasNotAttribute("attr", null));
+ }
+
+ @Tag("span")
+ static class NonTextComponent extends Component implements HasComponents {
+ public NonTextComponent(String text) {
+ getElement().setText(text);
+ }
+
+ public NonTextComponent(Component... components) {
+ add(components);
+ }
+ }
+
+ @Tag("span")
+ static class TextComponent extends Component {
+ public TextComponent(String text) {
+ super(Element.createText(text));
+ }
+
+ @Override
+ public boolean isVisible() {
+ // Workaround to bypass BasicUtils.kt _isVisible method that fails
+ // with UnsupportedOperationException if text node is not of type
+ // Text
+ return true;
+ }
+ }
+
+ @Tag("span")
+ static class HasTextComponent extends TextComponent implements HasText {
+
+ public HasTextComponent(String text) {
+ super(text);
+ }
+
+ @Override
+ public String getText() {
+ return HasText.super.getText().toUpperCase();
+ }
+ }
+
+ @Tag("article")
+ public static class Article extends HtmlComponent implements HasComponents {
+ public Article(Component... components) {
+ add(components);
+ }
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/NonGenericTestTester.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/NonGenericTestTester.java
new file mode 100644
index 000000000..95c33cd32
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/NonGenericTestTester.java
@@ -0,0 +1,31 @@
+/**
+ * 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;
+
+@Tests(TestComponentForConcreteTester.class)
+public class NonGenericTestTester
+ extends ComponentTester {
+
+ /**
+ * Wrap given component for testing.
+ *
+ * @param component
+ * target component
+ */
+ public NonGenericTestTester(TestComponentForConcreteTester component) {
+ super(component);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/SecurityTestConfig.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/SecurityTestConfig.java
new file mode 100644
index 000000000..044e6ccaa
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/SecurityTestConfig.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.vaadin.browserless;
+
+import java.util.List;
+import java.util.UUID;
+
+import com.testapp.security.LoginView;
+import com.testapp.security.ProtectedView;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+import com.vaadin.flow.router.RouteConfiguration;
+import com.vaadin.flow.server.VaadinServiceInitListener;
+import com.vaadin.flow.server.auth.MenuAccessControl;
+import com.vaadin.flow.server.auth.NavigationAccessControl;
+import com.vaadin.flow.spring.security.SpringMenuAccessControl;
+
+// Empty configuration class used only to be able to bootstrap spring
+// ApplicationContext
+class SecurityTestConfig {
+
+ @Configuration
+ public static class NavigationAccessControlConfig extends Commons {
+ // Registers test views and view access checker for testing purpose
+ @Bean
+ VaadinServiceInitListener setupViewSecurityScenario() {
+ return event -> {
+ RouteConfiguration routeConfiguration = RouteConfiguration
+ .forApplicationScope();
+ routeConfiguration.setAnnotatedRoute(LoginView.class);
+ routeConfiguration.setAnnotatedRoute(ProtectedView.class);
+ event.getSource().addUIInitListener(uiEvent -> {
+ NavigationAccessControl accessControl = new NavigationAccessControl();
+ accessControl.setLoginView(LoginView.class);
+ uiEvent.getUI().addBeforeEnterListener(accessControl);
+ });
+ };
+ }
+ }
+
+ private static class Commons {
+ @Bean
+ UserDetailsService mockUserDetailsService() {
+
+ return new UserDetailsService() {
+ @Override
+ public UserDetails loadUserByUsername(String username)
+ throws UsernameNotFoundException {
+ if ("user".equals(username)) {
+ return new User(username, UUID.randomUUID().toString(),
+ List.of(new SimpleGrantedAuthority(
+ "ROLE_SUPERUSER"),
+ new SimpleGrantedAuthority(
+ "ROLE_DEV")));
+ }
+ throw new UsernameNotFoundException(
+ "User " + username + " not exists");
+ }
+ };
+ }
+
+ @Bean
+ MenuAccessControl menuAccessControl() {
+ return new SpringMenuAccessControl();
+ }
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/SignalsTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/SignalsTest.java
new file mode 100644
index 000000000..c2536bcb9
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/SignalsTest.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.browserless;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import com.example.base.signals.SignalsView;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+@ViewPackages(packages = "com.example.base.signals")
+@Timeout(10)
+public class SignalsTest extends BrowserlessTest {
+
+ @Test
+ void attachedComponent_triggerSignal_effectEvaluatedSynchronously() {
+ var view = navigate(SignalsView.class);
+ var counterTester = test(view.counter);
+ Assertions.assertEquals("Counter: 0", counterTester.getText());
+
+ test(view.incrementButton).click();
+ Assertions.assertEquals("Counter: 1", counterTester.getText());
+ }
+
+ @Test
+ void detachedComponent_triggerSignal_effectEvaluatedOnAttach() {
+ var view = navigate(SignalsView.class);
+ var counterTester = test(view.counter);
+ Assertions.assertEquals("Counter: 0", counterTester.getText());
+ view.counter.removeFromParent();
+ Assertions.assertFalse(counterTester.isUsable());
+
+ test(view.incrementButton).click();
+ Assertions.assertEquals("Counter: 0", view.counter.getText());
+
+ view.add(view.counter);
+ Assertions.assertEquals("Counter: 1", view.counter.getText());
+ }
+
+ @Test
+ void attachedComponent_triggerSignalFromNonUIThread_effectEvaluatedAsynchronously() {
+ var view = navigate(SignalsView.class);
+ var counterTester = test(view.asyncCounter);
+ Assertions.assertEquals("Counter: 0", counterTester.getText());
+ CompletableFuture.runAsync(() -> {
+ view.asyncNumberSignal.incrementBy(10.0);
+ });
+ runPendingSignalsTasks();
+ Assertions.assertEquals("Counter: 10", counterTester.getText());
+ }
+
+ @Test
+ void attachedComponent_triggerSignalFromNonUIThreadThroughComponentEffect_effectEvaluatedAsynchronously() {
+ var view = navigate(SignalsView.class);
+ var counterTester = test(view.asyncCounter);
+ Assertions.assertEquals("Counter: 0", counterTester.getText());
+ test(view.quickBackgroundTaskButton).click();
+ runPendingSignalsTasks(300, TimeUnit.MILLISECONDS);
+ Assertions.assertEquals("Counter: 10", counterTester.getText());
+ }
+
+ @Test
+ void attachedComponent_slowEffect_effectEvaluatedAsynchronously() {
+ var view = navigate(SignalsView.class);
+ var counterTester = test(view.asyncWithDelayCounter);
+ Assertions.assertEquals("Counter: 0 (delayed)",
+ counterTester.getText());
+ test(view.slowBackgroundTaskButton).click();
+ Assertions.assertTrue(
+ runPendingSignalsTasks(300, TimeUnit.MILLISECONDS),
+ "Expected pending signals tasks to be run");
+ Assertions.assertEquals("Counter: 10 (delayed)",
+ counterTester.getText());
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/SpringBrowserlessBaseClassTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/SpringBrowserlessBaseClassTest.java
new file mode 100644
index 000000000..5bbfa7ca1
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/SpringBrowserlessBaseClassTest.java
@@ -0,0 +1,60 @@
+/**
+ * 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.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+
+import com.vaadin.browserless.mocks.MockSpringServletService;
+import com.vaadin.browserless.mocks.MockSpringVaadinSession;
+import com.vaadin.flow.server.VaadinRequest;
+import com.vaadin.flow.server.VaadinService;
+import com.vaadin.flow.server.VaadinSession;
+
+@ContextConfiguration(classes = SpringBrowserlessBaseClassTest.TestConfig.class)
+class SpringBrowserlessBaseClassTest extends SpringBrowserlessTest {
+
+ @Test
+ void extendingBaseClass_runTest_vaadinSpringMockingIsSetup() {
+ Assertions.assertNotNull(VaadinService.getCurrent(),
+ "Expecting VaadinService to be available up, but was not");
+ Assertions.assertInstanceOf(MockSpringServletService.class,
+ VaadinService.getCurrent(), "Expecting VaadinService to be "
+ + MockSpringServletService.class);
+
+ Assertions.assertNotNull(VaadinSession.getCurrent(),
+ "Expecting VaadinSession to be available up, but was not");
+ Assertions.assertInstanceOf(MockSpringVaadinSession.class,
+ VaadinSession.getCurrent(), "Expecting VaadinService to be "
+ + MockSpringVaadinSession.class);
+
+ Assertions.assertNotNull(VaadinRequest.getCurrent(),
+ "Expecting VaadinSession to be available up, but was not");
+ Assertions.assertInstanceOf(MockSpringVaadinSession.class,
+ VaadinSession.getCurrent(), "Expecting VaadinService to be "
+ + MockSpringVaadinSession.class);
+
+ }
+
+ // Empty configuration class used only to be able to bootstrap spring
+ // ApplicationContext
+ @Configuration
+ static class TestConfig {
+
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/SpringUnitSecurityTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/SpringUnitSecurityTest.java
new file mode 100644
index 000000000..72b1dd824
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/SpringUnitSecurityTest.java
@@ -0,0 +1,127 @@
+/**
+ * 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.security.Principal;
+
+import com.testapp.security.LoginView;
+import com.testapp.security.ProtectedView;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.security.test.context.support.WithAnonymousUser;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.context.ContextConfiguration;
+
+import com.vaadin.flow.server.VaadinRequest;
+import com.vaadin.flow.server.VaadinService;
+import com.vaadin.flow.server.auth.MenuAccessControl;
+import com.vaadin.flow.spring.security.SpringMenuAccessControl;
+
+@ContextConfiguration(classes = SecurityTestConfig.NavigationAccessControlConfig.class)
+@ViewPackages(packages = "com.testapp.security")
+class SpringUnitSecurityTest extends SpringBrowserlessTest {
+
+ @Test
+ @WithMockUser(username = "john", roles = { "DEV", "PO" })
+ void withMockUser_loggedUser_authenticationInformationAvailableOnRequest() {
+ VaadinRequest request = VaadinRequest.getCurrent();
+ Principal principal = request.getUserPrincipal();
+ Assertions.assertNotNull(principal,
+ "Principal should be provided by Spring Security, but was not found");
+ Assertions.assertEquals("john", principal.getName());
+
+ Assertions.assertTrue(request.isUserInRole("DEV"),
+ "Principal should have DEV role");
+ Assertions.assertTrue(request.isUserInRole("PO"),
+ "Principal should have PO role");
+ Assertions.assertFalse(request.isUserInRole("CEO"),
+ "Principal should not have CEO role");
+
+ Assertions.assertTrue(request.isUserInRole("ROLE_DEV"),
+ "Principal should have DEV role");
+ Assertions.assertTrue(request.isUserInRole("ROLE_PO"),
+ "Principal should have PO role");
+ Assertions.assertFalse(request.isUserInRole("CEO"),
+ "Principal should not have CEO role");
+ }
+
+ @Test
+ void withoutSecurity_noAuthenticationInformation() {
+ Principal principal = VaadinRequest.getCurrent().getUserPrincipal();
+ Assertions.assertNull(principal,
+ "Principal should not be present, but got " + principal);
+ }
+
+ @Test
+ @WithAnonymousUser
+ void witAnonymousUser_noAuthenticationInformation() {
+ Principal principal = VaadinRequest.getCurrent().getUserPrincipal();
+ Assertions.assertNull(principal,
+ "Principal should not be present, but got " + principal);
+ }
+
+ @Test
+ @WithUserDetails
+ void withUserDetails_loggedUser_authenticationInformationAvailableOnRequest() {
+ VaadinRequest request = VaadinRequest.getCurrent();
+ Principal principal = request.getUserPrincipal();
+ Assertions.assertNotNull(principal,
+ "Principal should be provided by Spring Security, but was not found");
+ Assertions.assertEquals("user", principal.getName());
+
+ Assertions.assertTrue(request.isUserInRole("DEV"),
+ "Principal should have DEV role");
+ Assertions.assertTrue(request.isUserInRole("ROLE_DEV"),
+ "Principal should have DEV role");
+ Assertions.assertTrue(request.isUserInRole("SUPERUSER"),
+ "Principal should have SUPERUSER role");
+ Assertions.assertTrue(request.isUserInRole("ROLE_SUPERUSER"),
+ "Principal should have SUPERUSER role");
+ Assertions.assertFalse(request.isUserInRole("ADMIN"),
+ "Principal should not have ADMIN role");
+ Assertions.assertFalse(request.isUserInRole("ROLE_ADMIN"),
+ "Principal should not have ADMIN role");
+ }
+
+ @Test
+ @WithMockUser(username = "john", roles = { "DEV", "PO" })
+ void withMockUser_landOnProtectedHomeView() {
+ Assertions.assertInstanceOf(ProtectedView.class, getCurrentView(),
+ "Logged user should land to protected home view");
+ }
+
+ @Test
+ @WithAnonymousUser
+ void withAnonymousUser_redirectToLogin() {
+ Assertions.assertInstanceOf(LoginView.class, getCurrentView(),
+ "Anonymous user should be redirect to login view");
+ }
+
+ @Test
+ void extendingBaseClass_runTest_menuAccessControlAvailable() {
+ MenuAccessControl menuAccessControl = VaadinService.getCurrent()
+ .getInstantiator().getMenuAccessControl();
+ Assertions.assertNotNull(menuAccessControl,
+ "Expecting MenuAccessControl to be available");
+ Assertions.assertInstanceOf(SpringMenuAccessControl.class,
+ menuAccessControl,
+ "Expecting menu access control to be SpringMenuAccessControl but was "
+ + menuAccessControl.getClass().getName());
+
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestComponent.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestComponent.java
new file mode 100644
index 000000000..f3869494b
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestComponent.java
@@ -0,0 +1,23 @@
+/**
+ * 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 com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
+
+@Tag("span")
+public class TestComponent extends Component {
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestComponentForConcreteTester.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestComponentForConcreteTester.java
new file mode 100644
index 000000000..1e5094702
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestComponentForConcreteTester.java
@@ -0,0 +1,23 @@
+/**
+ * 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 com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
+
+@Tag("span")
+public class TestComponentForConcreteTester extends Component {
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestCustomInstantiatorFactory.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestCustomInstantiatorFactory.java
new file mode 100644
index 000000000..3bcf1863f
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestCustomInstantiatorFactory.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.vaadin.browserless;
+
+import com.vaadin.flow.di.DefaultInstantiator;
+import com.vaadin.flow.di.Instantiator;
+import com.vaadin.flow.di.InstantiatorFactory;
+import com.vaadin.flow.server.VaadinService;
+
+/**
+ * A custom {@link InstantiatorFactory} to test
+ * {@link com.vaadin.flow.di.Lookup} initialization.
+ */
+public class TestCustomInstantiatorFactory implements InstantiatorFactory {
+
+ @Override
+ public Instantiator createInstantitor(VaadinService vaadinService) {
+ return new DefaultInstantiator(vaadinService);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestTester.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestTester.java
new file mode 100644
index 000000000..8a93e06a9
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TestTester.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.browserless;
+
+@Tests(TestComponent.class)
+public class TestTester extends ComponentTester {
+
+ /**
+ * Wrap given component for testing.
+ *
+ * @param component
+ * target component
+ */
+ public TestTester(T component) {
+ super(component);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/TesterResolutionTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TesterResolutionTest.java
new file mode 100644
index 000000000..2b48d1aff
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TesterResolutionTest.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.browserless;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
+
+@ComponentTesterPackages("com.vaadin.browserless")
+@ViewPackages(packages = "com.vaadin.browserless")
+public class TesterResolutionTest extends BrowserlessTest {
+
+ @Test
+ public void wrapTest_returnsTestWrap() {
+ TestComponent tc = new TestComponent();
+
+ Assertions.assertTrue(test(tc) instanceof TestTester);
+ }
+
+ @Test
+ public void wrapComponentExtendingTest_returnsTestWrap() {
+ MyTest mt = new MyTest();
+
+ Assertions.assertTrue(test(mt) instanceof TestTester);
+ }
+
+ @Test
+ public void wrapOtherComponent_returnsGenericComponentWrap() {
+ SpecialComponent sc = new SpecialComponent();
+ Assertions
+ .assertTrue(test(sc).getClass().equals(ComponentTester.class));
+ }
+
+ @Test
+ public void wrapTestComponentForConcreteWrapper_returnsNonGenericTestWrap() {
+ TestComponentForConcreteTester component = new TestComponentForConcreteTester();
+ Assertions.assertEquals(test(component).getClass(),
+ NonGenericTestTester.class);
+ }
+
+ @Test
+ void detectComponentType_resolvesComponentTypeThroughHierarchy() {
+ Assertions.assertEquals(Component.class,
+ detectComponentType(ComponentTester.class));
+ Assertions.assertEquals(TestComponent.class,
+ detectComponentType(MyTester.class));
+ Assertions.assertEquals(MyTest.class,
+ detectComponentType(MyExtendedTester.class));
+ Assertions.assertEquals(TestComponentForConcreteTester.class,
+ detectComponentType(NonGenericTestTester.class));
+ }
+
+ public static class MyTest extends TestComponent {
+ }
+
+ @Tag("div")
+ public static class SpecialComponent extends Component {
+ }
+
+ static class MyTester
+ extends ComponentTester {
+ public MyTester(Z component) {
+ super(component);
+ }
+ }
+
+ static class MyExtendedTester extends MyTester {
+ public MyExtendedTester(MyTest component) {
+ super(component);
+ }
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/TesterScanTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TesterScanTest.java
new file mode 100644
index 000000000..5af9a08da
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/TesterScanTest.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.browserless;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+// Ensure that scanning testers does not fail when a tester references
+// a component class that is not present on the classpath.
+public class TesterScanTest {
+
+ @Test
+ public void scanForTesters_testerForClassNotInClasspath_doNotThrowOnClassNotFoundException() {
+ // Loads a dummy tester annotated with @Tests using an FQN to a
+ // non-existing component class.
+ Assertions.assertDoesNotThrow(() -> BaseBrowserlessTest.scanForTesters(
+ "com.vaadin.browserless.dontscan.classnotfound"));
+ }
+
+ @Test
+ public void scanForTesters_testerForClassNotInClasspath_doNotThrowNoClassDefFound() {
+ // Loads a dummy tester annotated with @Tests referencing a class in
+ // another module with provided scope so the test itself is not able to
+ // load the class.
+ Assertions.assertDoesNotThrow(() -> BaseBrowserlessTest.scanForTesters(
+ "com.vaadin.browserless.dontscan.noclassdeffound"));
+ }
+
+ @Test
+ public void scanForTesters_testerForClassNotInClasspath_doNotThrowTypeNotPresentException() {
+ // Loads a dummy tester annotated with @Tests referencing a class in
+ // another module with provided scope so the test itself is not able to
+ // load the class.
+ Assertions.assertDoesNotThrow(() -> BaseBrowserlessTest.scanForTesters(
+ "com.vaadin.browserless.dontscan.typenotpresent"));
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/noclassdeffound/NoClassDefFoundComponent.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/noclassdeffound/NoClassDefFoundComponent.java
new file mode 100644
index 000000000..a5da1ab0e
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/noclassdeffound/NoClassDefFoundComponent.java
@@ -0,0 +1,23 @@
+/**
+ * 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.dontscan.noclassdeffound;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
+
+@Tag("span")
+public class NoClassDefFoundComponent extends Component {
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/noclassdeffound/NoClassDefFoundComponentTester.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/noclassdeffound/NoClassDefFoundComponentTester.java
new file mode 100644
index 000000000..0531bab4c
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/noclassdeffound/NoClassDefFoundComponentTester.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.vaadin.browserless.dontscan.noclassdeffound;
+
+import com.vaadin.browserless.ComponentTester;
+import com.vaadin.browserless.Tests;
+
+/**
+ * Dummy tester referencing a class from a third-party JAR that might not be
+ * available at runtime. This is used to verify that scanning for testers does
+ * not fail when the referenced component class is not on the classpath.
+ */
+@Tests(NoClassDefFoundComponent.class)
+public class NoClassDefFoundComponentTester
+ extends ComponentTester {
+
+ public NoClassDefFoundComponentTester(T component) {
+ super(component);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/typenotpresent/TypeNotPresentComponent.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/typenotpresent/TypeNotPresentComponent.java
new file mode 100644
index 000000000..fe3b87122
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/typenotpresent/TypeNotPresentComponent.java
@@ -0,0 +1,23 @@
+/**
+ * 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.dontscan.typenotpresent;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
+
+@Tag("span")
+public class TypeNotPresentComponent extends Component {
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/typenotpresent/TypeNotPresentComponentTester.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/typenotpresent/TypeNotPresentComponentTester.java
new file mode 100644
index 000000000..fac4c5525
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/dontscan/typenotpresent/TypeNotPresentComponentTester.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.vaadin.browserless.dontscan.typenotpresent;
+
+import com.vaadin.browserless.ComponentTester;
+import com.vaadin.browserless.Tests;
+import com.vaadin.flow.component.Component;
+
+/**
+ * Dummy tester whose annotation references a class from a third-party JAR that
+ * might not be available at runtime. This is used to verify that scanning for
+ * testers does not fail when the referenced component class is not on the
+ * classpath.
+ */
+@Tests(TypeNotPresentComponent.class)
+public class TypeNotPresentComponentTester extends ComponentTester {
+
+ public TypeNotPresentComponentTester(Component component) {
+ super(component);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/byannotatedclass/DiscoverRoutesInAnnotatedClassPackageTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/byannotatedclass/DiscoverRoutesInAnnotatedClassPackageTest.java
new file mode 100644
index 000000000..da0e046f5
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/byannotatedclass/DiscoverRoutesInAnnotatedClassPackageTest.java
@@ -0,0 +1,42 @@
+/**
+ * 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.viewscan.byannotatedclass;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.browserless.BrowserlessTest;
+import com.vaadin.browserless.ViewPackages;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.router.RouteBaseData;
+import com.vaadin.flow.server.VaadinService;
+
+@ViewPackages
+class DiscoverRoutesInAnnotatedClassPackageTest extends BrowserlessTest {
+
+ @Test
+ void extendingBaseClass_runTest_routesAreDiscovered() {
+ Set> routes = VaadinService.getCurrent()
+ .getRouter().getRegistry().getRegisteredRoutes().stream()
+ .map(RouteBaseData::getNavigationTarget)
+ .collect(Collectors.toSet());
+ Assertions.assertEquals(1, routes.size());
+ Assertions.assertTrue(routes.contains(ViewPackagesTestView.class));
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/byannotatedclass/ViewPackagesTestView.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/byannotatedclass/ViewPackagesTestView.java
new file mode 100644
index 000000000..a53dd3453
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/byannotatedclass/ViewPackagesTestView.java
@@ -0,0 +1,25 @@
+/**
+ * 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.viewscan.byannotatedclass;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.router.Route;
+
+@Tag("div")
+@Route("view-packages-test")
+public class ViewPackagesTestView extends Component {
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/byclasses/DiscoverRoutesInPackageByClassTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/byclasses/DiscoverRoutesInPackageByClassTest.java
new file mode 100644
index 000000000..e90f14c13
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/byclasses/DiscoverRoutesInPackageByClassTest.java
@@ -0,0 +1,45 @@
+/**
+ * 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.viewscan.byclasses;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.example.base.child.ChildView;
+import com.example.base.navigation.NavigationPostponeView;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.browserless.BrowserlessTest;
+import com.vaadin.browserless.ViewPackages;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.router.RouteBaseData;
+import com.vaadin.flow.server.VaadinService;
+
+@ViewPackages(classes = { ChildView.class, NavigationPostponeView.class })
+class DiscoverRoutesInPackageByClassTest extends BrowserlessTest {
+
+ @Test
+ void extendingBaseClass_runTest_routesAreDiscovered() {
+ Set> routes = VaadinService.getCurrent()
+ .getRouter().getRegistry().getRegisteredRoutes().stream()
+ .map(RouteBaseData::getNavigationTarget)
+ .collect(Collectors.toSet());
+ Assertions.assertEquals(2, routes.size());
+ Assertions.assertTrue(routes.contains(ChildView.class));
+ Assertions.assertTrue(routes.contains(NavigationPostponeView.class));
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/bypackagename/DiscoverRoutesInPackageByNameTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/bypackagename/DiscoverRoutesInPackageByNameTest.java
new file mode 100644
index 000000000..ab446f8ce
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/bypackagename/DiscoverRoutesInPackageByNameTest.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.browserless.viewscan.bypackagename;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.example.base.child.ChildView;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.browserless.BrowserlessTest;
+import com.vaadin.browserless.ViewPackages;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.router.RouteBaseData;
+import com.vaadin.flow.server.VaadinService;
+
+@ViewPackages(packages = "com.example.base.child")
+class DiscoverRoutesInPackageByNameTest extends BrowserlessTest {
+
+ @Test
+ void extendingBaseClass_runTest_routesAreDiscovered() {
+ Set> routes = VaadinService.getCurrent()
+ .getRouter().getRegistry().getRegisteredRoutes().stream()
+ .map(RouteBaseData::getNavigationTarget)
+ .collect(Collectors.toSet());
+ Assertions.assertEquals(1, routes.size());
+ Assertions.assertTrue(routes.contains(ChildView.class));
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/bypackagenameandclass/DiscoverRoutesInPackageByClassAndNameTest.java b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/bypackagenameandclass/DiscoverRoutesInPackageByClassAndNameTest.java
new file mode 100644
index 000000000..19e6123c1
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/browserless/viewscan/bypackagenameandclass/DiscoverRoutesInPackageByClassAndNameTest.java
@@ -0,0 +1,45 @@
+/**
+ * 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.viewscan.bypackagenameandclass;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.example.base.child.ChildView;
+import com.example.base.navigation.NavigationPostponeView;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.browserless.BrowserlessTest;
+import com.vaadin.browserless.ViewPackages;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.router.RouteBaseData;
+import com.vaadin.flow.server.VaadinService;
+
+@ViewPackages(classes = NavigationPostponeView.class, packages = "com.example.base.child")
+public class DiscoverRoutesInPackageByClassAndNameTest extends BrowserlessTest {
+
+ @Test
+ void extendingBaseClass_runTest_routesAreDiscovered() {
+ Set> routes = VaadinService.getCurrent()
+ .getRouter().getRegistry().getRegisteredRoutes().stream()
+ .map(RouteBaseData::getNavigationTarget)
+ .collect(Collectors.toSet());
+ Assertions.assertEquals(2, routes.size());
+ Assertions.assertTrue(routes.contains(ChildView.class));
+ Assertions.assertTrue(routes.contains(NavigationPostponeView.class));
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/accordion/AccordionTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/accordion/AccordionTesterTest.java
new file mode 100644
index 000000000..b7cc4d939
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/accordion/AccordionTesterTest.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.accordion;
+
+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 AccordionTesterTest extends BrowserlessTest {
+
+ AccordionView view;
+
+ @BeforeEach
+ public void registerView() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(AccordionView.class);
+ view = navigate(AccordionView.class);
+ }
+
+ @Test
+ void getPanelBySummary_returnsCorrectPanel() {
+ final AccordionTester wrap = test(view.accordion);
+ wrap.openDetails("Red");
+ Assertions.assertSame(view.redPanel, wrap.getPanel("Red"));
+ wrap.openDetails("Disabled");
+ Assertions.assertSame(view.disabledPanel, wrap.getPanel("Disabled"));
+ }
+
+ @Test
+ void closedPanel_getPanelThrows() {
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.accordion).getPanel("Green"));
+ }
+
+ @Test
+ void isOpen_seesCorrectPanel() {
+ view.accordion.open(view.redPanel);
+
+ final AccordionTester wrap = test(view.accordion);
+ Assertions.assertTrue(wrap.isOpen("Red"), "Red should be open");
+ Assertions.assertFalse(wrap.isOpen("Green"), "Only red should be open");
+
+ view.accordion.open(view.greenPanel);
+
+ Assertions.assertFalse(wrap.isOpen("Red"),
+ "Red should close after green is open");
+ }
+
+ @Test
+ void hasPanel_returnsTrueForExistingPanel() {
+ final AccordionTester wrap = test(view.accordion);
+ Assertions.assertTrue(wrap.hasPanel("Green"),
+ "Green panel should exist");
+ Assertions.assertFalse(wrap.hasPanel("Orange"),
+ "No Orange panel is added");
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/accordion/AccordionView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/accordion/AccordionView.java
new file mode 100644
index 000000000..2943cb52e
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/accordion/AccordionView.java
@@ -0,0 +1,50 @@
+/**
+ * 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.accordion;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HasComponents;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Span;
+import com.vaadin.flow.router.Route;
+
+@Tag("div")
+@Route(value = "accordion", registerAtStartup = false)
+public class AccordionView extends Component implements HasComponents {
+
+ Accordion accordion;
+
+ // for content testing
+ Div redDiv, greenDiv;
+ AccordionPanel redPanel, greenPanel, disabledPanel;
+
+ public AccordionView() {
+
+ accordion = new Accordion();
+
+ redDiv = new Div();
+
+ greenDiv = new Div();
+
+ redPanel = accordion.add("Red", redDiv);
+ greenPanel = accordion.add("Green", greenDiv);
+ disabledPanel = accordion.add("Disabled", new Span("Disabled panel"));
+ disabledPanel.setEnabled(false);
+
+ add(accordion);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/button/ButtonTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/button/ButtonTesterTest.java
new file mode 100644
index 000000000..e3450f41f
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/button/ButtonTesterTest.java
@@ -0,0 +1,138 @@
+/**
+ * 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.button;
+
+import java.util.concurrent.atomic.AtomicInteger;
+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.MetaKeys;
+import com.vaadin.browserless.ViewPackages;
+import com.vaadin.flow.component.ClickEvent;
+import com.vaadin.flow.router.RouteConfiguration;
+
+@ViewPackages
+public class ButtonTesterTest extends BrowserlessTest {
+
+ private ButtonView view;
+
+ @BeforeEach
+ public void registerView() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(ButtonView.class);
+ view = navigate(ButtonView.class);
+ }
+
+ @Test
+ public void buttonWithDisableOnClick_notUsableAfterClick() {
+ view.button.setDisableOnClick(true);
+
+ final ButtonTester button_ = test(ButtonTester.class, view.button);
+
+ Assertions.assertTrue(button_.isUsable(),
+ "Button should be usable before click");
+
+ button_.click();
+
+ Assertions.assertFalse(button_.isUsable(),
+ "Button should have been disabled after click");
+
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> button_.click(),
+ "Illegal state should be thrown for disabled button");
+ }
+
+ @Test
+ public void clickWithMiddleButton_middleButtonClickShouldBeRegistered() {
+ AtomicInteger mouseButton = new AtomicInteger(-1);
+ view.button
+ .addClickListener(event -> mouseButton.set(event.getButton()));
+
+ final ButtonTester button_ = test(ButtonTester.class, view.button);
+ button_.middleClick();
+
+ Assertions.assertEquals(1, mouseButton.get(),
+ "Click event should have sent with middle click");
+ }
+
+ @Test
+ public void clickWithRightButton_rightButtonClickShouldBeRegistered() {
+ AtomicInteger mouseButton = new AtomicInteger(-1);
+ view.button
+ .addClickListener(event -> mouseButton.set(event.getButton()));
+
+ final ButtonTester button_ = test(ButtonTester.class, view.button);
+ button_.rightClick();
+
+ Assertions.assertEquals(2, mouseButton.get(),
+ "Click event should have sent with right click");
+ }
+
+ @Test
+ public void normalClick_noMetaKeysMarkedAsUsed() {
+ AtomicReference event = new AtomicReference<>(null);
+ view.button.addClickListener(
+ clickEvent -> event.compareAndSet(null, clickEvent));
+
+ final ButtonTester button_ = test(ButtonTester.class, view.button);
+ button_.click();
+
+ Assertions.assertNotNull(event.get(),
+ "event should have fired and recorded");
+ Assertions.assertFalse(event.get().isCtrlKey(),
+ "Ctrl should not have been used");
+ Assertions.assertFalse(event.get().isShiftKey(),
+ "Shift should not have been used");
+ Assertions.assertFalse(event.get().isAltKey(),
+ "Alt should not have been used");
+ Assertions.assertFalse(event.get().isMetaKey(),
+ "Meta should not have been used");
+ }
+
+ @Test
+ public void clickWithMeta_metaKeysMarkedAsUsed() {
+ AtomicReference event = new AtomicReference<>(null);
+ view.button.addClickListener(clickEvent -> event.set(clickEvent));
+
+ final ButtonTester button_ = test(ButtonTester.class, view.button);
+ button_.click(new MetaKeys(true, true, true, true));
+
+ Assertions.assertNotNull(event.get(),
+ "event should have fired and recorded");
+ Assertions.assertTrue(event.get().isCtrlKey(),
+ "Ctrl should have been used");
+ Assertions.assertTrue(event.get().isShiftKey(),
+ "Shift should have been used");
+ Assertions.assertTrue(event.get().isAltKey(),
+ "Alt should have been used");
+ Assertions.assertTrue(event.get().isMetaKey(),
+ "Meta should have been used");
+
+ button_.click(new MetaKeys(true, true, false, false));
+ Assertions.assertTrue(event.get().isCtrlKey(),
+ "Ctrl should have been used");
+ Assertions.assertTrue(event.get().isShiftKey(),
+ "Shift should have been used");
+ Assertions.assertFalse(event.get().isAltKey(),
+ "Alt should not have been used");
+ Assertions.assertFalse(event.get().isMetaKey(),
+ "Meta should not have been used");
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/button/ButtonView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/button/ButtonView.java
new file mode 100644
index 000000000..ac8a0c6be
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/button/ButtonView.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.button;
+
+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 = "button", registerAtStartup = false)
+public class ButtonView extends Component implements HasComponents {
+ Button button;
+
+ public ButtonView() {
+ button = new Button();
+ add(button);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/charts/ChartTesterSeriesValueTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/charts/ChartTesterSeriesValueTest.java
new file mode 100644
index 000000000..542c09038
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/charts/ChartTesterSeriesValueTest.java
@@ -0,0 +1,351 @@
+/**
+ * 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.charts;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.flow.component.charts.model.Configuration;
+import com.vaadin.flow.component.charts.model.DataProviderSeries;
+import com.vaadin.flow.component.charts.model.DataSeries;
+import com.vaadin.flow.component.charts.model.DataSeriesItem;
+import com.vaadin.flow.component.charts.model.HeatSeries;
+import com.vaadin.flow.component.charts.model.ListSeries;
+import com.vaadin.flow.component.charts.model.TreeSeries;
+import com.vaadin.flow.component.charts.model.TreeSeriesItem;
+import com.vaadin.flow.data.provider.DataProvider;
+import com.vaadin.flow.data.provider.ListDataProvider;
+
+class ChartTesterSeriesValueTest {
+
+ private ChartTester tester = new ChartTester<>(new Chart());
+
+ @Test
+ void getValuesFromSeries_listSeries_getsValues() {
+ ListSeries series = setupListSeries();
+ Assertions.assertIterableEquals(tester.getValuesFromSeries(series),
+ List.of(series.getData()));
+ }
+
+ @Test
+ void getPointValue_listSeries_getsCorrectValue() {
+ ListSeries series = setupListSeries();
+ Assertions.assertEquals(series.getData()[0],
+ tester.getPointValueFromSeries(series, "Jan"));
+ Assertions.assertEquals(series.getData()[1],
+ tester.getPointValueFromSeries(series, "Feb"));
+ Assertions.assertEquals(series.getData()[2],
+ tester.getPointValueFromSeries(series, "Mar"));
+ Assertions.assertEquals(series.getData()[3],
+ tester.getPointValueFromSeries(series, "Apr"));
+ Assertions.assertEquals(series.getData()[4],
+ tester.getPointValueFromSeries(series, "May"));
+ Assertions.assertEquals(series.getData()[5],
+ tester.getPointValueFromSeries(series, "Jun"));
+ Assertions.assertEquals(series.getData()[6],
+ tester.getPointValueFromSeries(series, "Jul"));
+ Assertions.assertEquals(series.getData()[7],
+ tester.getPointValueFromSeries(series, "Aug"));
+ Assertions.assertEquals(series.getData()[8],
+ tester.getPointValueFromSeries(series, "Sep"));
+ Assertions.assertEquals(series.getData()[9],
+ tester.getPointValueFromSeries(series, "Oct"));
+ Assertions.assertEquals(series.getData()[10],
+ tester.getPointValueFromSeries(series, "Nov"));
+ Assertions.assertEquals(series.getData()[11],
+ tester.getPointValueFromSeries(series, "Dec"));
+ }
+
+ @Test
+ void getPointValue_listSeries_invalidCategoryThrows() {
+ ListSeries series = setupListSeries();
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> tester.getPointValueFromSeries(series, "XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("XYZ"));
+ Assertions.assertTrue(
+ exception.getMessage().contains("Invalid X-Axis category"));
+ }
+
+ @Test
+ void getPointValue_listSeries_missingXAxisCategoriesThrows() {
+ ListSeries series = new ListSeries("MyTest");
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> tester.getPointValueFromSeries(series, "XYZ"));
+ Assertions.assertTrue(exception.getMessage()
+ .contains("X-Axis categories not configured"));
+ }
+
+ @Test
+ void getValuesFromSeries_dataSeries_getsValues() {
+ DataSeries series = createDataSeries();
+
+ Assertions.assertIterableEquals(tester.getValuesFromSeries(series),
+ series.getData().stream().map(DataSeriesItem::getY)
+ .collect(Collectors.toList()));
+ }
+
+ @Test
+ void getPointValue_dataSeries_getsCorrectValue() {
+ DataSeries series = createDataSeries();
+ for (DataSeriesItem item : series.getData()) {
+ Assertions.assertEquals(item.getY(),
+ tester.getPointValueFromSeries(series, item.getName()));
+ }
+ }
+
+ @Test
+ void getPointValue_dataSeries_notExistingThrows() {
+ DataSeries series = createDataSeries();
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> tester.getPointValueFromSeries(series, "XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("XYZ"));
+ Assertions.assertTrue(
+ exception.getMessage().contains("Invalid DataSeriesItem name"));
+ }
+
+ @Test
+ void getValuesFromSeries_treeSeries_unsupported() {
+ TreeSeries series = setupTreeSeries();
+
+ UnsupportedOperationException exception = Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> tester.getValuesFromSeries(series));
+ Assertions.assertTrue(exception.getMessage()
+ .contains(TreeSeries.class.getSimpleName()));
+ Assertions.assertTrue(
+ exception.getMessage().contains("is not supported"));
+ }
+
+ @Test
+ void getPointValue_treeSeries_unsupported() {
+ TreeSeries series = setupTreeSeries();
+
+ UnsupportedOperationException exception = Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> tester.getValuesFromSeries(series));
+ Assertions.assertTrue(exception.getMessage()
+ .contains(TreeSeries.class.getSimpleName()));
+ Assertions.assertTrue(
+ exception.getMessage().contains("is not supported"));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void getValuesFromSeries_dataProviderSeries_getsValues() {
+ DataProviderSeries series = setupDataProviderSeries();
+ Collection orders = ((ListDataProvider) series
+ .getDataProvider()).getItems();
+
+ Assertions.assertIterableEquals(tester.getValuesFromSeries(series),
+ orders.stream().map(Order::getTotalPrice)
+ .collect(Collectors.toList()));
+ }
+
+ @Test
+ void getPointValue_dataProviderSeries_getsCorrectValue() {
+ DataProviderSeries series = setupDataProviderSeries();
+ Collection orders = ((ListDataProvider) series
+ .getDataProvider()).getItems();
+ for (Order item : orders) {
+ Assertions.assertEquals(item.getTotalPrice(), tester
+ .getPointValueFromSeries(series, item.getDescription()));
+ }
+ }
+
+ @Test
+ void getPointValue_dataProviderSeries_notExistingThrows() {
+ DataProviderSeries series = setupDataProviderSeries();
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> tester.getPointValueFromSeries(series, "XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("XYZ"));
+ Assertions.assertTrue(
+ exception.getMessage().contains("Invalid name for X-Axis"));
+ }
+
+ @Test
+ void getPointValue_dataProviderSeries_missingXAxisAttributeThrows() {
+ DataProviderSeries series = setupDataProviderSeries();
+ series.setX(null);
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> tester.getPointValueFromSeries(series, "XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("XYZ"));
+ Assertions.assertTrue(
+ exception.getMessage().contains("Invalid name for X-Axis"));
+ }
+
+ @Test
+ void getPointValue_dataProviderSeries_duplicatedXAxisAttributeThrows() {
+ DataProviderSeries series = setupDataProviderSeries();
+ series.setX(order -> "XYZ");
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> tester.getPointValueFromSeries(series, "XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("XYZ"));
+ Assertions.assertTrue(exception.getMessage()
+ .contains("multiple items with same X-Axis name"));
+ }
+
+ @Test
+ void getValuesFromSeries_heatSeries_unsupported() {
+ HeatSeries series = setupHeatSeries();
+ UnsupportedOperationException exception = Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> tester.getValuesFromSeries(series));
+ Assertions.assertTrue(exception.getMessage()
+ .contains(HeatSeries.class.getSimpleName()));
+ Assertions.assertTrue(
+ exception.getMessage().contains("is not supported"));
+ }
+
+ @Test
+ void getPointValue_heatSeries_unsupported() {
+ HeatSeries series = setupHeatSeries();
+
+ UnsupportedOperationException exception = Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> tester.getValuesFromSeries(series));
+ Assertions.assertTrue(exception.getMessage()
+ .contains(HeatSeries.class.getSimpleName()));
+ Assertions.assertTrue(
+ exception.getMessage().contains("is not supported"));
+ }
+
+ private ListSeries setupListSeries() {
+ ListSeries series = new ListSeries("Tokyo", 49.9, 71.5, 106.4, 129.2,
+ 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4);
+ Configuration config = tester.getComponent().getConfiguration();
+ config.addSeries(series);
+ config.getxAxis().setCategories("Jan", "Feb", "Mar", "Apr", "May",
+ "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
+ return series;
+ }
+
+ private HeatSeries setupHeatSeries() {
+ HeatSeries series = new HeatSeries("Sales per employee",
+ new Number[][] { { 0, 0, 0 }, { 0, 1, 19 }, { 0, 2, 8 },
+ { 0, 3, 24 }, { 0, 4, 67 }, { 1, 0, 92 }, { 1, 1, 58 },
+ { 1, 2, 78 }, { 1, 3, 117 }, { 1, 4, 48 }, { 2, 0, 35 },
+ { 2, 1, 15 }, { 2, 2, 123 }, { 2, 3, 64 }, { 2, 4, 52 },
+ { 3, 0, 72 }, { 3, 1, 132 }, { 3, 2, 114 },
+ { 3, 3, 19 }, { 3, 4, 16 }, { 4, 0, 38 }, { 4, 1, 5 },
+ { 4, 2, 8 }, { 4, 3, 117 }, { 4, 4, 115 }, { 5, 0, 88 },
+ { 5, 1, 32 }, { 5, 2, 12 }, { 5, 3, 6 }, { 5, 4, 120 },
+ { 6, 0, 13 }, { 6, 1, 44 }, { 6, 2, 88 }, { 6, 3, 98 },
+ { 6, 4, 96 }, { 7, 0, 31 }, { 7, 1, 1 }, { 7, 2, 82 },
+ { 7, 3, 32 }, { 7, 4, 30 }, { 8, 0, 85 }, { 8, 1, 97 },
+ { 8, 2, 123 }, { 8, 3, 64 }, { 8, 4, 84 }, { 9, 0, 47 },
+ { 9, 1, 114 }, { 9, 2, 31 }, { 9, 3, 48 },
+ { 9, 4, 91 } });
+ tester.getComponent().getConfiguration().addSeries(series);
+ return series;
+ }
+
+ private TreeSeries setupTreeSeries() {
+ TreeSeries series = new TreeSeries();
+ TreeSeriesItem apples = new TreeSeriesItem("A", "Apples");
+ apples.setColorIndex(0);
+ TreeSeriesItem bananas = new TreeSeriesItem("B", "Bananas");
+ bananas.setColorIndex(2);
+ TreeSeriesItem oranges = new TreeSeriesItem("O", "Oranges");
+ oranges.setColorIndex(3);
+ TreeSeriesItem anneA = new TreeSeriesItem("Anne", apples, 5);
+ TreeSeriesItem rickA = new TreeSeriesItem("Rick", apples, 3);
+ TreeSeriesItem peterA = new TreeSeriesItem("Peter", apples, 4);
+ TreeSeriesItem anneB = new TreeSeriesItem("Anne", bananas, 4);
+ TreeSeriesItem rickB = new TreeSeriesItem("Rick", bananas, 10);
+ TreeSeriesItem peterB = new TreeSeriesItem("Peter", bananas, 1);
+ TreeSeriesItem anneO = new TreeSeriesItem("Anne", oranges, 1);
+ TreeSeriesItem rickO = new TreeSeriesItem("Rick", oranges, 3);
+ TreeSeriesItem peterO = new TreeSeriesItem("Peter", oranges, 3);
+ TreeSeriesItem susanne = new TreeSeriesItem("Susanne", 2);
+ susanne.setParent("Kiwi");
+ susanne.setColorIndex(4);
+ series.addAll(apples, bananas, oranges, anneA, rickA, peterA, anneB,
+ rickB, peterB, anneO, rickO, peterO, susanne);
+ tester.getComponent().getConfiguration().addSeries(series);
+ return series;
+ }
+
+ private DataSeries createDataSeries() {
+ DataSeries series = new DataSeries();
+ series.add(new DataSeriesItem("Chrome", 61.41));
+ series.add(new DataSeriesItem("Internet Explorer", 11.84));
+ series.add(new DataSeriesItem("Firefox", 10.85));
+ series.add(new DataSeriesItem("Edge", 4.67));
+ series.add(new DataSeriesItem("Safari", 4.18));
+ series.add(new DataSeriesItem("Sogou Explorer", 1.64));
+ series.add(new DataSeriesItem("Opera", 6.2));
+ series.add(new DataSeriesItem("QQ", 1.2));
+ series.add(new DataSeriesItem("Others", 2.61));
+ tester.getComponent().getConfiguration().addSeries(series);
+ return series;
+ }
+
+ private DataProviderSeries setupDataProviderSeries() {
+ List orders = new ArrayList<>();
+ orders.add(new Order("Domain Name", 3, 7.99));
+ orders.add(new Order("SSL Certificate", 1, 119.00));
+ orders.add(new Order("Web Hosting", 1, 19.95));
+ orders.add(new Order("Email Box", 20, 0.15));
+ orders.add(new Order("E-Commerce Setup", 1, 25.00));
+ orders.add(new Order("Technical Support", 1, 50.00));
+ DataProvider dataProvider = new ListDataProvider<>(orders);
+ DataProviderSeries series = new DataProviderSeries<>(
+ dataProvider, Order::getTotalPrice);
+ series.setX(Order::getDescription);
+ tester.getComponent().getConfiguration().addSeries(series);
+ return series;
+ }
+
+ private static class Order {
+
+ private final String description;
+ private final int quantity;
+ private final double unitPrice;
+
+ public Order(String description, int quantity, double unitPrice) {
+ this.description = description;
+ this.quantity = quantity;
+ this.unitPrice = unitPrice;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public double getUnitPrice() {
+ return unitPrice;
+ }
+
+ public double getTotalPrice() {
+ return unitPrice * quantity;
+ }
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/charts/ChartTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/charts/ChartTesterTest.java
new file mode 100644
index 000000000..7f85007d7
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/charts/ChartTesterTest.java
@@ -0,0 +1,284 @@
+/**
+ * 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.charts;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+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.CommercialTesterWrappers;
+import com.vaadin.browserless.ViewPackages;
+import com.vaadin.flow.component.charts.model.ListSeries;
+import com.vaadin.flow.component.charts.model.Series;
+import com.vaadin.flow.router.RouteConfiguration;
+
+@ViewPackages
+class ChartTesterTest extends BrowserlessTest
+ implements CommercialTesterWrappers {
+
+ ColumnChartView view;
+
+ @BeforeEach
+ public void registerView() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(ColumnChartView.class);
+ view = navigate(ColumnChartView.class);
+ }
+
+ @Test
+ void getSeriesValues_notUsable_throws() {
+ view.chart.setVisible(false);
+ ChartTester chartTester = test(view.chart);
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> chartTester.getSeriesValues(0));
+ Assertions.assertTrue(exception.getMessage().contains("not usable"));
+ exception = Assertions.assertThrows(IllegalStateException.class,
+ () -> chartTester.getSeriesValues("Berlin"));
+ Assertions.assertTrue(exception.getMessage().contains("not usable"));
+ }
+
+ @Test
+ void getSeriesValues_byIndex_getsSeriesValues() {
+ Assertions.assertIterableEquals(test(view.chart).getSeriesValues(0),
+ List.of(view.tokyo.getData()));
+ Assertions.assertIterableEquals(test(view.chart).getSeriesValues(1),
+ List.of(view.newYork.getData()));
+ Assertions.assertIterableEquals(test(view.chart).getSeriesValues(2),
+ List.of(view.london.getData()));
+ Assertions.assertIterableEquals(test(view.chart).getSeriesValues(3),
+ List.of(view.berlin.getData()));
+ }
+
+ @Test
+ void getSeriesValues_byName_getsSeriesValues() {
+ Assertions.assertIterableEquals(
+ test(view.chart).getSeriesValues("Tokyo"),
+ List.of(view.tokyo.getData()));
+ Assertions.assertIterableEquals(
+ test(view.chart).getSeriesValues("New York"),
+ List.of(view.newYork.getData()));
+ Assertions.assertIterableEquals(
+ test(view.chart).getSeriesValues("London"),
+ List.of(view.london.getData()));
+ Assertions.assertIterableEquals(
+ test(view.chart).getSeriesValues("Berlin"),
+ List.of(view.berlin.getData()));
+ }
+
+ @Test
+ void getSeriesValues_invalidIndex_throws() {
+ ChartTester chartTester = test(view.chart);
+ Assertions.assertThrows(IndexOutOfBoundsException.class,
+ () -> chartTester.getSeriesValues(-1));
+ Assertions.assertThrows(IndexOutOfBoundsException.class,
+ () -> chartTester.getSeriesValues(5));
+ }
+
+ @Test
+ void getSeriesValues_notExistingSeries_throws() {
+ ChartTester chartTester = test(view.chart);
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> chartTester.getSeriesValues("Rome"));
+ Assertions.assertTrue(exception.getMessage().contains("Rome"));
+ Assertions
+ .assertTrue(exception.getMessage().contains("does not exists"));
+ }
+
+ @Test
+ void getSeriesValues_duplicatedSeries_throws() {
+ String seriesName = view.tokyo.getName();
+ ListSeries duplicated = new ListSeries(seriesName,
+ view.tokyo.getData());
+ view.chart.getConfiguration().addSeries(duplicated);
+ ChartTester chartTester = test(view.chart);
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> chartTester.getSeriesValues(seriesName));
+ Assertions.assertTrue(exception.getMessage().contains(seriesName));
+ Assertions
+ .assertTrue(exception.getMessage().contains("Multiple series"));
+ }
+
+ @Test
+ void getPointValue_byIndex_getsPointValue() {
+ for (int i = 0; i < view.categories.length; i++) {
+ Assertions.assertEquals(
+ test(view.chart).getPointValue(2, view.categories[i]),
+ view.london.getData()[i],
+ "Point " + view.categories[i] + " from series " + i);
+ }
+ }
+
+ @Test
+ void getPointValue_byName_getsPointValue() {
+ for (int i = 0; i < view.categories.length; i++) {
+ Assertions
+ .assertEquals(
+ test(view.chart).getPointValue(
+ view.london.getName(), view.categories[i]),
+ view.london.getData()[i]);
+ }
+ }
+
+ @Test
+ void getPointValue_notExistingItemName_throws() {
+ ChartTester chartTester = test(view.chart);
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> chartTester.getPointValue("Berlin", "XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("Invalid"));
+ exception = Assertions.assertThrows(IllegalArgumentException.class,
+ () -> chartTester.getPointValue(0, "XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("Invalid"));
+ }
+
+ @Test
+ void getPointValue_notUsable_throws() {
+ view.chart.setVisible(false);
+ ChartTester chartTester = test(view.chart);
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> chartTester.getPointValue(0, "Jan"));
+ Assertions.assertTrue(exception.getMessage().contains("not usable"));
+ exception = Assertions.assertThrows(IllegalStateException.class,
+ () -> chartTester.getPointValue("Berlin", "Jan"));
+ Assertions.assertTrue(exception.getMessage().contains("not usable"));
+ }
+
+ @Test
+ void getPointValue_duplicatedName_throws() {
+ view.categories[2] = "Feb";
+ view.chart.getConfiguration().getxAxis().setCategories(view.categories);
+
+ ChartTester chartTester = test(view.chart);
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> chartTester.getPointValue(0, "Feb"));
+ Assertions.assertTrue(exception.getMessage().contains("Feb"));
+ Assertions.assertTrue(exception.getMessage().contains("multiple"));
+ exception = Assertions.assertThrows(IllegalStateException.class,
+ () -> chartTester.getPointValue("Berlin", "Feb"));
+ Assertions.assertTrue(exception.getMessage().contains("Feb"));
+ Assertions.assertTrue(exception.getMessage().contains("multiple"));
+ }
+
+ @Test
+ void clickLegendItem_notUsable_throws() {
+ view.chart.setVisible(false);
+ ChartTester chartTester = test(view.chart);
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> chartTester.clickLegendItem("Berlin"));
+ Assertions.assertTrue(exception.getMessage().contains("not usable"));
+ }
+
+ @Test
+ void clickLegendItem_notExisting_throws() {
+ ChartTester chartTester = test(view.chart);
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> chartTester.clickLegendItem("Rome"));
+ Assertions.assertTrue(exception.getMessage().contains("Rome"));
+ Assertions
+ .assertTrue(exception.getMessage().contains("does not exist"));
+ }
+
+ @Test
+ void clickLegendItem_eventFired() {
+ // SeriesLegendItemClickEvent
+ // PointLegendItemClickEvent ???
+ AtomicReference seriesLegend = new AtomicReference<>();
+ view.chart.addSeriesLegendItemClickListener(
+ ev -> seriesLegend.set(ev.getSeries()));
+
+ test(view.chart).clickLegendItem("Berlin");
+ Assertions.assertSame(view.berlin, seriesLegend.get());
+ }
+
+ @Test
+ void clickPoint_notUsable_throws() {
+ view.chart.setVisible(false);
+ ChartTester chartTester = test(view.chart);
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> chartTester.clickPoint("Berlin", "Feb"));
+ Assertions.assertTrue(exception.getMessage().contains("not usable"));
+ }
+
+ @Test
+ void clickPoint_notExisting_throws() {
+ ChartTester chartTester = test(view.chart);
+ IllegalArgumentException exception = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> chartTester.clickPoint("Rome", "Feb"));
+ Assertions.assertTrue(exception.getMessage().contains("Rome"));
+ Assertions
+ .assertTrue(exception.getMessage().contains("does not exist"));
+
+ exception = Assertions.assertThrows(IllegalArgumentException.class,
+ () -> chartTester.clickPoint("London", "XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("XYZ"));
+ Assertions.assertTrue(exception.getMessage().contains("Invalid"));
+ }
+
+ @Test
+ void clickPoint_eventsFired() {
+ // SeriesClick
+ // PointClick
+ AtomicReference series = new AtomicReference<>();
+ AtomicReference pointSeries = new AtomicReference<>();
+ AtomicInteger point = new AtomicInteger();
+ view.chart.addSeriesClickListener(ev -> series.set(ev.getSeries()));
+ view.chart.addPointClickListener(ev -> {
+ pointSeries.set(ev.getSeries());
+ point.set(ev.getItemIndex());
+ });
+
+ test(view.chart).clickPoint("Berlin", "Feb");
+ Assertions.assertSame(view.berlin, series.get());
+ Assertions.assertSame(view.berlin, pointSeries.get());
+ Assertions.assertEquals(1, point.get());
+ }
+
+ @Test
+ void clickChart_notUsable_throws() {
+ view.chart.setVisible(false);
+ ChartTester chartTester = test(view.chart);
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class, chartTester::clickChart);
+ Assertions.assertTrue(exception.getMessage().contains("not usable"));
+ }
+
+ @Test
+ void clickChart_eventFired() {
+ AtomicBoolean chartClicked = new AtomicBoolean();
+ view.chart.addChartClickListener(ev -> chartClicked.set(true));
+
+ test(view.chart).clickChart();
+ Assertions.assertTrue(chartClicked.get());
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/charts/ColumnChartView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/charts/ColumnChartView.java
new file mode 100644
index 000000000..3afa3d23d
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/charts/ColumnChartView.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.charts;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HasComponents;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.component.charts.model.ChartType;
+import com.vaadin.flow.component.charts.model.Configuration;
+import com.vaadin.flow.component.charts.model.Crosshair;
+import com.vaadin.flow.component.charts.model.ListSeries;
+import com.vaadin.flow.component.charts.model.Tooltip;
+import com.vaadin.flow.component.charts.model.XAxis;
+import com.vaadin.flow.component.charts.model.YAxis;
+import com.vaadin.flow.router.Route;
+
+@Tag("div")
+@Route(value = "column-chart", registerAtStartup = false)
+public class ColumnChartView extends Component implements HasComponents {
+
+ final Chart chart;
+ final ListSeries tokyo = new ListSeries("Tokyo", 49.9, 71.5, 106.4, 129.2,
+ 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4);
+ final ListSeries newYork = new ListSeries("New York", 83.6, 78.8, 98.5,
+ 93.4, 106.0, 84.5, 105.0, 104.3, 91.2, 83.5, 106.6, 92.3);
+ final ListSeries london = new ListSeries("London", 48.9, 38.8, 39.3, 41.4,
+ 47.0, 48.3, 59.0, 59.6, 52.4, 65.2, 59.3, 51.2);
+ final ListSeries berlin = new ListSeries("Berlin", 42.4, 33.2, 34.5, 39.7,
+ 52.6, 75.5, 57.4, 60.4, 47.6, 39.1, 46.8, 51.1);
+ final String[] categories = new String[] { "Jan", "Feb", "Mar", "Apr",
+ "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+
+ public ColumnChartView() {
+ chart = new Chart();
+
+ Configuration configuration = chart.getConfiguration();
+ configuration.setTitle("Monthly Average Rainfall");
+ configuration.setSubTitle("Source: WorldClimate.com");
+ chart.getConfiguration().getChart().setType(ChartType.COLUMN);
+
+ configuration.addSeries(tokyo);
+ configuration.addSeries(newYork);
+ configuration.addSeries(london);
+ configuration.addSeries(berlin);
+
+ XAxis x = new XAxis();
+ x.setCrosshair(new Crosshair());
+ x.setCategories(categories);
+ configuration.addxAxis(x);
+
+ YAxis y = new YAxis();
+ y.setMin(0);
+ y.setTitle("Rainfall (mm)");
+ configuration.addyAxis(y);
+
+ Tooltip tooltip = new Tooltip();
+ tooltip.setShared(true);
+ configuration.setTooltip(tooltip);
+
+ add(chart);
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/checkbox/CheckboxGroupTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/checkbox/CheckboxGroupTesterTest.java
new file mode 100644
index 000000000..46eb15b64
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/checkbox/CheckboxGroupTesterTest.java
@@ -0,0 +1,194 @@
+/**
+ * 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.checkbox;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+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 CheckboxGroupTesterTest extends BrowserlessTest {
+
+ CheckboxView view;
+
+ @BeforeEach
+ public void registerView() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(CheckboxView.class);
+ view = navigate(CheckboxView.class);
+ }
+
+ @Test
+ void selectItem_selectCorrectItem() {
+ test(view.checkboxGroup).selectItem("test-bar");
+ assertContainsExactlyInAnyOrder(Set.of(view.items.get("bar")),
+ test(view.checkboxGroup).getSelected());
+
+ test(view.checkboxGroup).selectItem("test-jay");
+ assertContainsExactlyInAnyOrder(
+ Set.of(view.items.get("bar"), view.items.get("jay")),
+ test(view.checkboxGroup).getSelected());
+ }
+
+ @Test
+ void selectItems_multipleItems_itemsSelected() {
+ test(view.checkboxGroup).selectItems("test-bar", "test-jay");
+ assertContainsExactlyInAnyOrder(
+ Set.of(view.items.get("bar"), view.items.get("jay")),
+ test(view.checkboxGroup).getSelected());
+ }
+
+ @Test
+ void selectItems_collectionOfItems_itemsSelected() {
+ test(view.checkboxGroup).selectItems(List.of("test-bar", "test-jay"));
+ assertContainsExactlyInAnyOrder(
+ Set.of(view.items.get("bar"), view.items.get("jay")),
+ test(view.checkboxGroup).getSelected());
+ }
+
+ @Test
+ void selectAll_allItemSelected() {
+ test(view.checkboxGroup).selectAll();
+ assertContainsExactlyInAnyOrder(view.items.values(),
+ test(view.checkboxGroup).getSelected());
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Test
+ void deselectItem_deselectCorrectItem() {
+ view.checkboxGroup.setValue(new HashSet<>(view.items.values()));
+
+ test(view.checkboxGroup).deselectItem("test-bar");
+ Assertions.assertEquals(
+ Set.of(view.items.get("foo"), view.items.get("baz"),
+ view.items.get("jay")),
+ test(view.checkboxGroup).getSelected());
+
+ test(view.checkboxGroup).deselectItem("test-jay");
+ assertContainsExactlyInAnyOrder(
+ Set.of(view.items.get("foo"), view.items.get("baz")),
+ test(view.checkboxGroup).getSelected());
+
+ }
+
+ @Test
+ void deselectItems_multipleItems_itemsDeselected() {
+ view.checkboxGroup.setValue(new HashSet<>(view.items.values()));
+
+ test(view.checkboxGroup).deselectItems("test-jay", "test-bar");
+ assertContainsExactlyInAnyOrder(
+ Set.of(view.items.get("foo"), view.items.get("baz")),
+ test(view.checkboxGroup).getSelected());
+ }
+
+ @Test
+ void deselectItems_collectionOfItems_itemsDeselected() {
+ view.checkboxGroup.setValue(new HashSet<>(view.items.values()));
+
+ test(view.checkboxGroup).deselectItems(List.of("test-jay", "test-bar"));
+ assertContainsExactlyInAnyOrder(
+ Set.of(view.items.get("foo"), view.items.get("baz")),
+ test(view.checkboxGroup).getSelected());
+ }
+
+ @Test
+ void deselectAll_noItemsSelected() {
+ view.checkboxGroup.setValue(new HashSet<>(view.items.values()));
+
+ test(view.checkboxGroup).deselectAll();
+ Set selectedItems = test(view.checkboxGroup)
+ .getSelected();
+ Assertions.assertTrue(selectedItems.isEmpty(),
+ "Expecting no elements to be selected, but got "
+ + selectedItems);
+ }
+
+ @Test
+ void selectItem_notExisting_throws() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> test(view.checkboxGroup).selectItem("jay"));
+ }
+
+ @Test
+ void deselectItem_notExisting_throws() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> test(view.checkboxGroup).deselectItem("jay"));
+ }
+
+ @Test
+ void selectItem_itemDisabled_throws() {
+ view.checkboxGroup
+ .setItemEnabledProvider(n -> n.getName().startsWith("b"));
+
+ // Items enabled, should work
+ test(view.checkboxGroup).selectItem("test-bar");
+ test(view.checkboxGroup).selectItem("test-baz");
+
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.checkboxGroup).selectItem("test-foo"));
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.checkboxGroup).selectItem("test-jay"));
+ }
+
+ @Test
+ void deselectItem_itemDisabled_throws() {
+ view.checkboxGroup
+ .setItemEnabledProvider(n -> n.getName().startsWith("b"));
+
+ // Items enabled, should work
+ test(view.checkboxGroup).deselectItem("test-bar");
+ test(view.checkboxGroup).deselectItem("test-baz");
+
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.checkboxGroup).deselectItem("test-foo"));
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.checkboxGroup).deselectItem("test-jay"));
+ }
+
+ @Test
+ void readOnly_isNotUsable() {
+ view.checkboxGroup.setReadOnly(true);
+
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.checkboxGroup).selectItem("test-foo"));
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.checkboxGroup).deselectItem("test-foo"));
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.checkboxGroup).selectAll());
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.checkboxGroup).deselectAll());
+
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private void assertContainsExactlyInAnyOrder(Collection expected,
+ Collection actual) {
+ Assertions.assertEquals(expected.size(), actual.size(), "Expected "
+ + expected.size() + " elements, but got " + actual.size());
+ Assertions.assertTrue(
+ expected.containsAll(actual) && actual.containsAll(expected));
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/checkbox/CheckboxTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/checkbox/CheckboxTesterTest.java
new file mode 100644
index 000000000..90c027671
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/checkbox/CheckboxTesterTest.java
@@ -0,0 +1,106 @@
+/**
+ * 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.checkbox;
+
+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 CheckboxTesterTest extends BrowserlessTest {
+
+ CheckboxView view;
+
+ @BeforeEach
+ public void registerView() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(CheckboxView.class);
+ view = navigate(CheckboxView.class);
+ }
+
+ @Test
+ void readOnlyCheckbox_isNotUsable() {
+ view.checkbox.setReadOnly(true);
+ Assertions.assertFalse(test(view.checkbox).isUsable(),
+ "Readonly checkbox should not be usable");
+ }
+
+ @Test
+ void click_usable_valueChanges() {
+ Assertions.assertFalse(view.checkbox.getValue(),
+ "Expecting checkbox initial state not to be checked");
+
+ test(view.checkbox).click();
+ Assertions.assertTrue(view.checkbox.getValue(),
+ "Expecting checkbox to be checked, but was not");
+
+ test(view.checkbox).click();
+ Assertions.assertFalse(view.checkbox.getValue(),
+ "Expecting checkbox not to be checked, but was");
+
+ }
+
+ @Test
+ void click_usable_checkedChangeFired() {
+ AtomicBoolean checkedChange = new AtomicBoolean();
+ view.checkbox.getElement().addPropertyChangeListener("checked",
+ ev -> checkedChange.set(true));
+
+ Assertions.assertFalse(view.checkbox.getValue(),
+ "Expecting checkbox not to be checked, but was");
+
+ test(view.checkbox).click();
+ Assertions.assertTrue(checkedChange.get(),
+ "Expected checked change event to be fired, but was not");
+ Assertions.assertTrue(view.checkbox.getValue(),
+ "Expecting checkbox not to be checked, but was");
+ }
+
+ @Test
+ void click_disabled_throws() {
+ view.checkbox.setEnabled(false);
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.checkbox)::click);
+ }
+
+ @Test
+ void click_disabledByProperty_throws() {
+ view.checkbox.setDisabled(true);
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.checkbox)::click);
+ }
+
+ @Test
+ void click_invisible_throws() {
+ view.checkbox.setVisible(false);
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.checkbox)::click);
+ }
+
+ @Test
+ void click_readOnly_throws() {
+ view.checkbox.setReadOnly(true);
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.checkbox)::click);
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/checkbox/CheckboxView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/checkbox/CheckboxView.java
new file mode 100644
index 000000000..adaca696a
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/checkbox/CheckboxView.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.checkbox;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Function;
+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 CheckboxView extends Component implements HasComponents {
+
+ Checkbox checkbox = new Checkbox();
+
+ Map items = Stream.of("foo", "bar", "baz", "jay")
+ .collect(Collectors.toMap(Function.identity(), Name::new,
+ (a, b) -> a, LinkedHashMap::new));
+
+ CheckboxGroup checkboxGroup = new CheckboxGroup<>();
+
+ public CheckboxView() {
+ add(checkbox);
+
+ checkboxGroup.setItems(items.values());
+ checkboxGroup.setItemLabelGenerator(item -> "test-" + item);
+ add(checkboxGroup);
+ }
+
+ 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/combobox/ComboBoxTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/ComboBoxTesterTest.java
new file mode 100644
index 000000000..515f29494
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/ComboBoxTesterTest.java
@@ -0,0 +1,77 @@
+/**
+ * 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.combobox;
+
+import java.util.Arrays;
+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
+public class ComboBoxTesterTest extends BrowserlessTest {
+
+ ComboBoxView view;
+
+ @BeforeEach
+ void init() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(ComboBoxView.class);
+ view = navigate(ComboBoxView.class);
+ }
+
+ @Test
+ void getSuggestionItems_noFilter_allItemsReturned() {
+ final List suggestions = test(view.combo)
+ .getSuggestionItems();
+ Assertions.assertIterableEquals(view.items, suggestions);
+ }
+
+ @Test
+ void getSuggestions_noFilter_allItemsReturned() {
+ final List suggestions = test(view.combo).getSuggestions();
+ Assertions.assertIterableEquals(Arrays.asList("test-foo", "test-bar"),
+ suggestions);
+ }
+
+ @Test
+ void setFilter_getSuggestions_filterIsApplied() {
+ test(view.combo).setFilter("fo");
+ final List suggestions = test(view.combo).getSuggestions();
+ Assertions.assertEquals(1, suggestions.size());
+ Assertions.assertEquals("test-foo", suggestions.get(0));
+ }
+
+ @Test
+ void selectItem_selectsCorrectItem() {
+ Assertions.assertNull(test(view.combo).getSelected());
+
+ test(view.combo).selectItem("test-foo");
+
+ Assertions.assertSame(view.items.get(0),
+ test(view.combo).getSelected());
+
+ test(view.combo).selectItem(null);
+
+ Assertions.assertNull(test(view.combo).getSelected(),
+ "Selecting null should clear selection");
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/ComboBoxView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/ComboBoxView.java
new file mode 100644
index 000000000..962cefee1
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/ComboBoxView.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.combobox;
+
+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 = "combo", registerAtStartup = false)
+public class ComboBoxView extends Component implements HasComponents {
+
+ ComboBox combo;
+ List items = Arrays.asList(new Name("foo"), new Name("bar"));
+
+ public ComboBoxView() {
+ combo = new ComboBox<>("TestBox");
+ combo.setItems(items);
+ combo.setItemLabelGenerator(item -> "test-" + item.toString());
+ add(combo);
+ }
+
+ 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/combobox/MultiSelectComboBoxTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/MultiSelectComboBoxTesterTest.java
new file mode 100644
index 000000000..15e42815f
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/MultiSelectComboBoxTesterTest.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.combobox;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+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 MultiSelectComboBoxTesterTest extends BrowserlessTest {
+
+ MultiSelectComboBoxView view;
+
+ @BeforeEach
+ void init() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(MultiSelectComboBoxView.class);
+ view = navigate(MultiSelectComboBoxView.class);
+ }
+
+ @Test
+ void getSuggestionItems_noFilter_allItemsReturned() {
+ final List suggestions = test(view.combo)
+ .getSuggestionItems();
+ Assertions.assertIterableEquals(view.items, suggestions);
+ }
+
+ @Test
+ void getSuggestions_noFilter_allItemsReturned() {
+ final List suggestions = test(view.combo).getSuggestions();
+ Assertions.assertIterableEquals(Arrays.asList("test-foo", "test-bar"),
+ suggestions);
+ }
+
+ @Test
+ void setFilter_getSuggestions_filterIsApplied() {
+ test(view.combo).setFilter("fo");
+ final List suggestions = test(view.combo).getSuggestions();
+ Assertions.assertEquals(1, suggestions.size());
+ Assertions.assertEquals("test-foo", suggestions.get(0));
+ }
+
+ @Test
+ void selectItem_selectsCorrectItem() {
+ Assertions.assertTrue(test(view.combo).getSelected().isEmpty());
+
+ test(view.combo).selectItem("test-foo");
+
+ Assertions.assertIterableEquals(Set.of(view.items.get(0)),
+ test(view.combo).getSelected());
+
+ test(view.combo).selectItem("test-foo", "test-bar");
+
+ Assertions.assertTrue(
+ test(view.combo).getSelected().contains(view.items.get(0)));
+ Assertions.assertTrue(
+ test(view.combo).getSelected().contains(view.items.get(1)));
+
+ test(view.combo).selectItem(null);
+
+ Assertions.assertTrue(test(view.combo).getSelected().isEmpty(),
+ "Selecting null should clear selection");
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/MultiSelectComboBoxView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/MultiSelectComboBoxView.java
new file mode 100644
index 000000000..5e7dd2240
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/combobox/MultiSelectComboBoxView.java
@@ -0,0 +1,57 @@
+/**
+ * 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.combobox;
+
+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 = "multicombo", registerAtStartup = false)
+public class MultiSelectComboBoxView extends Component
+ implements HasComponents {
+
+ MultiSelectComboBox combo;
+ List items = Arrays.asList(new Name("foo"), new Name("bar"));
+
+ public MultiSelectComboBoxView() {
+ combo = new MultiSelectComboBox<>("TestBox");
+ combo.setItems(items);
+ combo.setItemLabelGenerator(item -> "test-" + item.toString());
+ add(combo);
+ }
+
+ 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/confirmdialog/ConfirmDialogTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/confirmdialog/ConfirmDialogTesterTest.java
new file mode 100644
index 000000000..a3ac8275f
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/confirmdialog/ConfirmDialogTesterTest.java
@@ -0,0 +1,137 @@
+/**
+ * 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.confirmdialog;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+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.Component;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.router.RouteConfiguration;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@ViewPackages
+class ConfirmDialogTesterTest extends BrowserlessTest {
+
+ ConfirmDialogView view;
+ ConfirmDialogTester wrap;
+
+ @BeforeEach
+ void init() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(ConfirmDialogView.class);
+ view = navigate(ConfirmDialogView.class);
+ wrap = test(view.dialog);
+ }
+
+ @Test
+ void cancelNotAvailable_throwsException() {
+ wrap.open();
+ assertThrows(IllegalStateException.class, wrap::cancel);
+ }
+
+ @Test
+ void rejectNotAvailable_throwsException() {
+ wrap.open();
+ assertThrows(IllegalStateException.class, wrap::reject);
+ }
+
+ @Test
+ void cancelAvailable_cancelEventClosesDialog() {
+ AtomicInteger close = new AtomicInteger(0);
+ view.dialog.setCancelButton("cancel", event -> close.incrementAndGet());
+ wrap.open();
+
+ wrap.cancel();
+
+ Assertions.assertEquals(1, close.get());
+ Assertions.assertFalse(view.dialog.isOpened());
+ Assertions.assertFalse(view.dialog.isAttached(),
+ "Confirm dialog should be detached");
+ }
+
+ @Test
+ void rejectAvailable_cancelEventClosesDialog() {
+ AtomicInteger rejects = new AtomicInteger(0);
+ view.dialog.setRejectButton("reject",
+ event -> rejects.incrementAndGet());
+ wrap.open();
+
+ wrap.reject();
+
+ Assertions.assertEquals(1, rejects.get());
+ Assertions.assertFalse(view.dialog.isOpened());
+ Assertions.assertFalse(view.dialog.isAttached(),
+ "Confirm dialog should be detached");
+ }
+
+ @Test
+ void confirmButton_ClosesDialog() {
+ AtomicInteger confirm = new AtomicInteger(0);
+ view.dialog.addConfirmListener(event -> confirm.incrementAndGet());
+ wrap.open();
+
+ wrap.confirm();
+
+ Assertions.assertEquals(1, confirm.get());
+ Assertions.assertFalse(view.dialog.isOpened());
+ Assertions.assertFalse(view.dialog.isAttached(),
+ "Confirm dialog should be detached");
+ }
+
+ @Test
+ void programmaticallyClose_dialogIsDetached() {
+ wrap.open();
+
+ view.dialog.close();
+
+ Assertions.assertFalse(view.dialog.isAttached(),
+ "Confirm dialog should be detached");
+ }
+
+ @Test
+ void headerText_getHeaderReturnsCorrect() {
+ String header = "Test String";
+ view.dialog.setHeader(header);
+
+ wrap.open();
+
+ Assertions.assertEquals(header, wrap.getHeader());
+ Assertions.assertThrows(IllegalStateException.class,
+ wrap::getHeaderElement);
+ }
+
+ @Test
+ void headerElement_correctElementIsReturned() {
+ Head header = new Head();
+ view.dialog.setHeader(header);
+ wrap.open();
+ Assertions.assertEquals(header.getElement(), wrap.getHeaderElement());
+ Assertions.assertNull(wrap.getHeader(),
+ "No string header should be available");
+ }
+
+ @Tag("div")
+ private static class Head extends Component {
+
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/confirmdialog/ConfirmDialogView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/confirmdialog/ConfirmDialogView.java
new file mode 100644
index 000000000..a71973f4c
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/confirmdialog/ConfirmDialogView.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.confirmdialog;
+
+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 = "confirm-dialog", registerAtStartup = false)
+public class ConfirmDialogView extends Component implements HasComponents {
+
+ ConfirmDialog dialog;
+
+ public ConfirmDialogView() {
+ dialog = new ConfirmDialog();
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuTesterTest.java
new file mode 100644
index 000000000..dc20bdcc2
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuTesterTest.java
@@ -0,0 +1,392 @@
+/**
+ * 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.contextmenu;
+
+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.component.html.Div;
+import com.vaadin.flow.router.RouteConfiguration;
+
+@ViewPackages
+class ContextMenuTesterTest extends BrowserlessTest {
+
+ ContextMenuView view;
+
+ @BeforeEach
+ void init() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(ContextMenuView.class);
+ view = navigate(ContextMenuView.class);
+ }
+
+ @Test
+ void openCloseMenu_menuIsAttachedAndDetached() {
+ Assertions.assertFalse(view.menu.isAttached(),
+ "closed context menu should not be attached to the UI");
+ test(view.menu).open();
+ Assertions.assertTrue(view.menu.isAttached(),
+ "context menu should be attached to the UI, but was not");
+
+ test(view.menu).close();
+ Assertions.assertFalse(view.menu.isAttached(),
+ "context menu should be detached from the UI, but was not");
+ }
+
+ @Test
+ void programmaticallyClose_menuIsDetached() {
+ test(view.menu).open();
+
+ view.menu.close();
+
+ Assertions.assertFalse(view.menu.isAttached(),
+ "context menu should be detached from the UI, but was not");
+ }
+
+ @Test
+ void openMenu_alreadyOpen_throws() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+ IllegalStateException exception = Assertions
+ .assertThrows(IllegalStateException.class, menu_::open);
+ Assertions.assertTrue(exception.getMessage().contains("already open"));
+ }
+
+ @Test
+ void closeMenu_menuNotOpened_throws() {
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.menu)::close);
+ }
+
+ @Test
+ void clickItem_byText_actionExecuted() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ 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() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> menu_.clickItem("XYZ"));
+ }
+
+ @Test
+ void clickItem_menuNotOpened_throws() {
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.menu).clickItem("Bar"));
+ }
+
+ @Test
+ void clickItem_multipleMatches_throws() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> menu_.clickItem("Duplicated"));
+ Assertions.assertTrue(exception.getMessage()
+ .contains("Expecting a single menu item"));
+ }
+
+ @Test
+ void clickItem_checkable_checkStatusChanges() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ menu_.clickItem("Checkable");
+ Assertions.assertIterableEquals(List.of("Checkable"),
+ view.clickedItems);
+ Assertions.assertTrue(view.checkableItem.isChecked(),
+ "Item should be checked but was not");
+
+ menu_.clickItem("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() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class, () -> menu_.clickItem("Disabled"));
+ Assertions.assertTrue(exception.getMessage().contains("Menu item"));
+ Assertions.assertTrue(exception.getMessage().contains("is not usable"));
+
+ Assertions.assertTrue(view.clickedItems.isEmpty(),
+ "Listener should not have been notified");
+ }
+
+ @Test
+ void clickItem_hidden_throws() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class, () -> menu_.clickItem("Hidden"));
+ Assertions.assertTrue(exception.getMessage().contains("Menu item"));
+ Assertions.assertTrue(exception.getMessage().contains("is not usable"));
+ Assertions.assertTrue(view.clickedItems.isEmpty(),
+ "Listener should not have been notified");
+ }
+
+ @Test
+ void clickItem_nested_executeAction() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ 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() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ 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() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ IllegalStateException exception = Assertions
+ .assertThrows(IllegalStateException.class, () -> menu_
+ .clickItem("Hierarchical", "NestedDisabled", "Level3"));
+ Assertions.assertTrue(exception.getMessage().contains("Menu item"));
+ Assertions.assertTrue(exception.getMessage().contains("is not usable"));
+
+ exception = Assertions.assertThrows(IllegalStateException.class,
+ () -> menu_.clickItem("Hierarchical", "NestedInvisible",
+ "Level3"));
+ Assertions.assertTrue(exception.getMessage().contains("Menu item"));
+ Assertions.assertTrue(exception.getMessage().contains("is not usable"));
+ }
+
+ @Test
+ void clickItem_byIndex_executesAction() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ 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() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> menu_.clickItem(1, 2));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> menu_.clickItem(8, 5));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> menu_.clickItem(8, 1, 5));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> menu_.clickItem(8, 1, 0, 4));
+ }
+
+ @Test
+ void clickItem_byNestedIndexNotUsableParent_throws() {
+ // Hierarchical / NestedDisabled / Level3
+ test(view.menu).open();
+
+ IllegalStateException exception = Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> test(view.menu).clickItem(7, 3, 0));
+ Assertions.assertTrue(exception.getMessage().contains("Menu item"));
+ Assertions.assertTrue(exception.getMessage().contains("is not usable"));
+ }
+
+ @Test
+ void clickItem_byNestedIndexes_executesAction() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ menu_.clickItem(8, 0);
+ Assertions.assertIterableEquals(List.of("Hierarchical / Level2"),
+ view.clickedItems);
+
+ view.clickedItems.clear();
+ menu_.clickItem(8, 1, 0);
+ Assertions.assertIterableEquals(
+ List.of("Hierarchical / NestedSubMenu / Level3"),
+ view.clickedItems);
+
+ }
+
+ @Test
+ void isItemChecked_byText_getCheckedStatus() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ Assertions.assertFalse(menu_.isItemChecked("Checkable"),
+ "Checkable item should not be checked by default, but result is true");
+
+ view.checkableItem.setChecked(true);
+ Assertions.assertTrue(menu_.isItemChecked("Checkable"),
+ "Checkable item is checked, but result is false");
+
+ view.checkableItem.setChecked(false);
+ Assertions.assertFalse(menu_.isItemChecked("Checkable"),
+ "Checkable item is not checked, but result is true");
+ }
+
+ @Test
+ void isItemChecked_byIndex_getCheckedStatus() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ Assertions.assertFalse(menu_.isItemChecked(6),
+ "Checkable item should not be checked by default, but result is true");
+
+ view.checkableItem.setChecked(true);
+ Assertions.assertTrue(menu_.isItemChecked(6),
+ "Checkable item is checked, but result is false");
+
+ view.checkableItem.setChecked(false);
+ Assertions.assertFalse(menu_.isItemChecked(6),
+ "Checkable item is not checked, but result is true");
+ }
+
+ @Test
+ void isItemChecked_nestedByText_getCheckedStatus() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ 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() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ Assertions.assertTrue(menu_.isItemChecked(8, 2),
+ "Checkable item should be checked by default, but result is false");
+
+ view.nestedCheckableItem.setChecked(false);
+ Assertions.assertFalse(menu_.isItemChecked(8, 2),
+ "Checkable item is not checked, but result is true");
+
+ view.nestedCheckableItem.setChecked(true);
+ Assertions.assertTrue(menu_.isItemChecked(8, 2),
+ "Checkable item is checked, but result is false");
+ }
+
+ @Test
+ void isItemChecked_notCheckableItem_throws() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ 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(8, 1));
+ }
+
+ @Test
+ void isItemChecked_menuNotOpened_throws() {
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.menu).isItemChecked("Checkable"));
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> test(view.menu).isItemChecked(5));
+ }
+
+ @Test
+ void isItemChecked_notExisting_throws() {
+ ContextMenuTester menu_ = test(view.menu);
+ menu_.open();
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> menu_.isItemChecked("XYZ"));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> menu_.isItemChecked(22));
+ }
+
+ @Test
+ void openAndFind_ContextMenuItemsCanBeAccessed() {
+ var menuTester = test(view.menu);
+ menuTester.open();
+ var div = menuTester.find(Div.class).withText("Component Item")
+ .single();
+ Assertions.assertTrue(div.isAttached());
+
+ menuTester.close();
+ div = menuTester.find(Div.class).withText("Component Item").single();
+ Assertions.assertFalse(div.isAttached());
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuView.java
new file mode 100644
index 000000000..3f3e6fc30
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuView.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.flow.component.contextmenu;
+
+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.html.Div;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.router.Route;
+
+@Tag("div")
+@Route(value = "contextmenu", registerAtStartup = false)
+public class ContextMenuView extends Component implements HasComponents {
+
+ final MenuItem checkableItem;
+ final MenuItem nestedCheckableItem;
+ final List clickedItems = new ArrayList<>();
+ final ContextMenu menu;
+
+ public ContextMenuView() {
+ Span assignee = new Span();
+ menu = new ContextMenu();
+ menu.setTarget(assignee);
+ 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"));
+ menu.addItem(new VerticalLayout(new Div("Component Item")),
+ click -> clickedItems.add("Component Item"));
+ checkableItem = menu.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(assignee);
+ }
+
+ @Tag("span")
+ private static class Span extends Component {
+
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datepicker/DatePickerTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datepicker/DatePickerTesterTest.java
new file mode 100644
index 000000000..eae09a71c
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datepicker/DatePickerTesterTest.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.datepicker;
+
+import java.time.LocalDate;
+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 DatePickerTesterTest extends BrowserlessTest {
+
+ DatePickerView view;
+
+ @BeforeEach
+ public void registerView() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(DatePickerView.class);
+ view = navigate(DatePickerView.class);
+ }
+
+ @Test
+ void invalidValue_overMaxDate_throwsIllegalArgument() {
+ view.picker.setMax(LocalDate.of(1995, 1, 1));
+
+ assertThrows(IllegalArgumentException.class,
+ () -> test(view.picker).setValue(LocalDate.of(1995, 1, 5)));
+ }
+
+ @Test
+ void invalidValue_underMinDate_throwsIllegalArgument() {
+ view.picker.setMin(LocalDate.of(1995, 1, 5));
+
+ assertThrows(IllegalArgumentException.class,
+ () -> test(view.picker).setValue(LocalDate.of(1995, 1, 1)));
+ }
+
+ @Test
+ void setValue_eventIsFired_valueIsSet() {
+
+ AtomicReference value = new AtomicReference<>(null);
+
+ view.picker.addValueChangeListener(
+ (HasValue.ValueChangeListener>) event -> {
+ if (event.isFromClient()) {
+ value.compareAndSet(null, event.getValue());
+ }
+ });
+
+ final LocalDate newValue = LocalDate.of(1995, 1, 5);
+ test(view.picker).setValue(newValue);
+
+ Assertions.assertEquals(newValue, value.get());
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datepicker/DatePickerView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datepicker/DatePickerView.java
new file mode 100644
index 000000000..afa7b35ca
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datepicker/DatePickerView.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.datepicker;
+
+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 = "date", registerAtStartup = false)
+public class DatePickerView extends Component implements HasComponents {
+ DatePicker picker;
+
+ public DatePickerView() {
+ picker = new DatePicker();
+ add(picker);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datetimepicker/DateTimePickerTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datetimepicker/DateTimePickerTesterTest.java
new file mode 100644
index 000000000..e756c4717
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datetimepicker/DateTimePickerTesterTest.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.datetimepicker;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+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 DateTimePickerTesterTest extends BrowserlessTest {
+
+ DateTimePickerView view;
+
+ @BeforeEach
+ public void registerView() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(DateTimePickerView.class);
+ view = navigate(DateTimePickerView.class);
+ }
+
+ @Test
+ void invalidValue_overMaxDate_throwsIllegalArgument() {
+ view.picker.setMax(
+ LocalDateTime.of(LocalDate.of(1995, 1, 1), LocalTime.NOON));
+
+ assertThrows(IllegalArgumentException.class,
+ () -> test(view.picker).setValue(LocalDateTime
+ .of(LocalDate.of(1995, 1, 5), LocalTime.MIDNIGHT)));
+ }
+
+ @Test
+ void invalidValue_underMinDate_throwsIllegalArgument() {
+ view.picker.setMin(
+ LocalDateTime.of(LocalDate.of(1995, 1, 5), LocalTime.NOON));
+
+ assertThrows(IllegalArgumentException.class,
+ () -> test(view.picker).setValue(LocalDateTime
+ .of(LocalDate.of(1995, 1, 1), LocalTime.MIDNIGHT)));
+ }
+
+ @Test
+ void setValue_eventIsFired_valueIsSet() {
+
+ AtomicReference value = new AtomicReference<>(null);
+
+ view.picker.addValueChangeListener(
+ (HasValue.ValueChangeListener>) event -> {
+ if (event.isFromClient()) {
+ value.compareAndSet(null, event.getValue());
+ }
+ });
+
+ final LocalDateTime newValue = LocalDateTime
+ .of(LocalDate.of(1995, 1, 5), LocalTime.NOON);
+ test(view.picker).setValue(newValue);
+
+ Assertions.assertEquals(newValue, value.get());
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datetimepicker/DateTimePickerView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datetimepicker/DateTimePickerView.java
new file mode 100644
index 000000000..152c83f37
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/datetimepicker/DateTimePickerView.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.datetimepicker;
+
+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 = "date-time", registerAtStartup = false)
+public class DateTimePickerView extends Component implements HasComponents {
+ DateTimePicker picker;
+
+ public DateTimePickerView() {
+ picker = new DateTimePicker();
+ add(picker);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/details/DetailsTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/details/DetailsTesterTest.java
new file mode 100644
index 000000000..77d949f54
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/details/DetailsTesterTest.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.details;
+
+import java.util.ArrayList;
+import java.util.List;
+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 DetailsTesterTest extends BrowserlessTest {
+
+ DetailsView view;
+
+ @BeforeEach
+ public void registerView() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(DetailsView.class);
+ view = navigate(DetailsView.class);
+ }
+
+ @Test
+ void openDetails_contentVisible() {
+ AtomicBoolean listenerInvoked = new AtomicBoolean();
+ view.details.addOpenedChangeListener(ev -> listenerInvoked.set(true));
+
+ test(view.details).openDetails();
+ Assertions.assertTrue(listenerInvoked.get());
+ Assertions.assertTrue(view.details.isOpened(),
+ "Contents should be visible after opening details");
+ }
+
+ @Test
+ void openDetails_notUsable_throws() {
+ view.details.setEnabled(false);
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.details)::openDetails);
+ }
+
+ @Test
+ void openDetails_alreadyOpen_throws() {
+ view.details.setOpened(true);
+ AtomicBoolean listenerInvoked = new AtomicBoolean();
+ view.details.addOpenedChangeListener(ev -> listenerInvoked.set(true));
+
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.details)::openDetails);
+ Assertions.assertFalse(listenerInvoked.get());
+ }
+
+ @Test
+ void closeDetails_contentHidden() {
+ view.details.setOpened(true);
+
+ AtomicBoolean listenerInvoked = new AtomicBoolean();
+ view.details.addOpenedChangeListener(ev -> listenerInvoked.set(true));
+
+ test(view.details).closeDetails();
+ Assertions.assertTrue(listenerInvoked.get());
+ Assertions.assertFalse(view.details.isOpened(),
+ "Contents should not be visible after closing details");
+ }
+
+ @Test
+ void closeDetails_notUsable_throws() {
+ view.details.setEnabled(false);
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.details)::closeDetails);
+ }
+
+ @Test
+ void closeDetails_alreadyClosed_throws() {
+ AtomicBoolean listenerInvoked = new AtomicBoolean();
+ view.details.addOpenedChangeListener(ev -> listenerInvoked.set(true));
+
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.details)::closeDetails);
+ Assertions.assertFalse(listenerInvoked.get());
+ }
+
+ @Test
+ void toggleDetails_notUsable_throws() {
+ view.details.setEnabled(false);
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.details)::toggleDetails);
+ }
+
+ @Test
+ void toggleDetails_detailsVisibilityChanges() {
+ List stateChanges = new ArrayList<>();
+ view.details
+ .addOpenedChangeListener(ev -> stateChanges.add(ev.isOpened()));
+
+ test(view.details).toggleDetails();
+ Assertions.assertIterableEquals(List.of(true), stateChanges);
+
+ test(view.details).toggleDetails();
+ Assertions.assertIterableEquals(List.of(true, false), stateChanges);
+
+ test(view.details).toggleDetails();
+ Assertions.assertIterableEquals(List.of(true, false, true),
+ stateChanges);
+ }
+
+ @Test
+ void isOpen_getsDetailsOpenInfo() {
+ Assertions.assertFalse(test(view.details).isOpen());
+
+ view.details.setOpened(true);
+ Assertions.assertTrue(test(view.details).isOpen());
+
+ view.details.setOpened(false);
+ Assertions.assertFalse(test(view.details).isOpen());
+ }
+
+ @Test
+ void isOpen_notUsable_throws() {
+ view.details.setEnabled(false);
+ Assertions.assertThrows(IllegalStateException.class,
+ test(view.details)::isOpen);
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/details/DetailsView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/details/DetailsView.java
new file mode 100644
index 000000000..c7931e16e
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/details/DetailsView.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.details;
+
+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 = "details", registerAtStartup = false)
+public class DetailsView extends Component implements HasComponents {
+
+ Details details;
+ DetailsContents contents;
+
+ public DetailsView() {
+ contents = new DetailsContents();
+ details = new Details("Members", contents);
+ add(details);
+ }
+
+ @Tag("div")
+ public static class DetailsContents extends Component {
+
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/dialog/DialogTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/dialog/DialogTesterTest.java
new file mode 100644
index 000000000..fd135406c
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/dialog/DialogTesterTest.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.flow.component.dialog;
+
+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.ModalityMode;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.button.ButtonTester;
+import com.vaadin.flow.router.RouteConfiguration;
+
+@ViewPackages
+class DialogTesterTest extends BrowserlessTest {
+
+ DialogView view;
+ DialogTester dialog_;
+
+ @BeforeEach
+ void init() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(DialogView.class);
+ view = navigate(DialogView.class);
+ dialog_ = test(view.dialog);
+ }
+
+ @Test
+ void openAndClose_dialogIsAttachedAndDetached() {
+ dialog_.open();
+ Assertions.assertTrue(dialog_.isUsable(),
+ "Dialog should be attached on open");
+
+ dialog_.close();
+ Assertions.assertFalse(dialog_.isUsable(),
+ "Dialog should not be usable after close");
+ Assertions.assertFalse(view.dialog.isAttached(),
+ "Dialog should be detached on close");
+ }
+
+ @Test
+ void programmaticallyClose_dialogIsDetached() {
+ dialog_.open();
+
+ view.dialog.close();
+
+ Assertions.assertFalse(view.dialog.isAttached(),
+ "Dialog should be detached on close");
+ }
+
+ @Test
+ void modalDialog_visual_doNotBlockUIComponents() {
+ view.dialog.setModality(ModalityMode.VISUAL);
+ dialog_.open();
+ ButtonTester button_ = test(view.button);
+ Assertions.assertTrue(button_.isUsable(),
+ "Default VISUAL modal dialog should not block button");
+ }
+
+ @Test
+ void modalDialog_strict_blocksUIComponents() {
+ view.dialog.setModality(ModalityMode.STRICT);
+ dialog_.open();
+ ButtonTester button_ = test(view.button);
+ Assertions.assertFalse(button_.isUsable(),
+ "Dialog should be modal by default blocking button");
+
+ dialog_.close();
+
+ Assertions.assertTrue(button_.isUsable(),
+ "Closing dialog should enable button");
+ }
+
+ @Test
+ void nonModalDialog_UIComponentsUsable() {
+ view.dialog.setModality(ModalityMode.MODELESS);
+ dialog_.open();
+ ButtonTester button_ = test(view.button);
+ Assertions.assertTrue(button_.isUsable(),
+ "Non-modal dialog should not block button");
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/dialog/DialogView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/dialog/DialogView.java
new file mode 100644
index 000000000..0e2d350eb
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/dialog/DialogView.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.dialog;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HasComponents;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.router.Route;
+
+@Tag("div")
+@Route(value = "dialog", registerAtStartup = false)
+public class DialogView extends Component implements HasComponents {
+
+ Dialog dialog;
+ Button button;
+
+ public DialogView() {
+ dialog = new Dialog();
+ button = new Button();
+ add(button);
+ }
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/Address.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/Address.java
new file mode 100644
index 000000000..f17ec176c
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/Address.java
@@ -0,0 +1,90 @@
+/**
+ * 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;
+
+/**
+ * Copied from grid integration-tests.
+ */
+@SuppressWarnings("serial")
+public class Address implements Serializable {
+
+ private String streetAddress = "";
+ private Integer postalCode = null;
+ private String city = "";
+ private Country country = null;
+ private int number;
+
+ public Address() {
+
+ }
+
+ public Address(String streetAddress, int postalCode, String city,
+ Country country) {
+ setStreet(streetAddress);
+ setPostalCode(postalCode);
+ setCity(city);
+ setCountry(country);
+ }
+
+ @Override
+ public String toString() {
+ return "Address [streetAddress=" + streetAddress + ", postalCode="
+ + postalCode + ", city=" + city + ", country=" + country + "]";
+ }
+
+ public int getNumber() {
+ return number;
+ }
+
+ public void setNumber(int number) {
+ this.number = number;
+ }
+
+ public String getStreet() {
+ return streetAddress;
+ }
+
+ public void setStreet(String streetAddress) {
+ this.streetAddress = streetAddress;
+ }
+
+ public Integer getPostalCode() {
+ return postalCode;
+ }
+
+ public void setPostalCode(Integer postalCode) {
+ this.postalCode = postalCode;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public Country getCountry() {
+ return country;
+ }
+
+ public void setCountry(Country country) {
+ this.country = country;
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java
new file mode 100644
index 000000000..0c8e4f6a4
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java
@@ -0,0 +1,256 @@
+/**
+ * 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.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+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.Component;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.notification.Notification;
+import com.vaadin.flow.router.RouteConfiguration;
+
+@ViewPackages
+class BasicGridTesterTest extends BrowserlessTest {
+
+ BasicGridView view;
+
+ @BeforeEach
+ void init() {
+ RouteConfiguration.forApplicationScope()
+ .setAnnotatedRoute(BasicGridView.class);
+
+ view = navigate(BasicGridView.class);
+ }
+
+ @Test
+ void basicGrid_verifyColumnContent() {
+ Assertions.assertEquals(2, test(view.basicGrid).size());
+
+ Assertions.assertTrue(test(view.basicGrid).getSelected().isEmpty());
+
+ Assertions.assertEquals("Jorma",
+ test(view.basicGrid).getCellText(0, 0));
+ // second column is hidden
+ Assertions.assertEquals("46", test(view.basicGrid).getCellText(0, 1));
+
+ Assertions.assertEquals("Maya", test(view.basicGrid).getCellText(1, 0));
+ // second column is hidden
+ Assertions.assertEquals("18", test(view.basicGrid).getCellText(1, 1));
+ }
+
+ @Test
+ void basicGrid_selectionOnClick() {
+ Assertions.assertTrue(test(view.basicGrid).getSelected().isEmpty());
+
+ test(view.basicGrid).clickRow(0);
+ Assertions.assertEquals(1, test(view.basicGrid).getSelected().size());
+ Assertions.assertSame(view.person1,
+ test(view.basicGrid).getSelected().iterator().next());
+
+ test(view.basicGrid).clickRow(1);
+ Assertions.assertEquals(1, test(view.basicGrid).getSelected().size());
+ Assertions.assertSame(view.person2,
+ test(view.basicGrid).getSelected().iterator().next());
+ }
+
+ @Test
+ void basicGrid_deselectSelectedOnClick() {
+ Assertions.assertTrue(test(view.basicGrid).getSelected().isEmpty());
+
+ test(view.basicGrid).clickRow(0);
+ Assertions.assertEquals(1, test(view.basicGrid).getSelected().size());
+ Assertions.assertSame(view.person1,
+ test(view.basicGrid).getSelected().iterator().next());
+
+ test(view.basicGrid).clickRow(0);
+ Assertions.assertTrue(test(view.basicGrid).getSelected().isEmpty(),
+ "Clicking selected row should deselect");
+ }
+
+ @Test
+ void basicGrid_selectWillChangeSelection() {
+ test(view.basicGrid).select(1);
+ Assertions.assertEquals(1, test(view.basicGrid).getSelected().size());
+ Assertions.assertSame(view.person2,
+ test(view.basicGrid).getSelected().iterator().next());
+
+ test(view.basicGrid).select(0);
+ Assertions.assertEquals(1, test(view.basicGrid).getSelected().size(),
+ "Single select should only change selection.");
+ Assertions.assertSame(view.person1,
+ test(view.basicGrid).getSelected().iterator().next());
+ }
+
+ @Test
+ void basicGrid_headerContent() {
+ Assertions.assertEquals("First Name", test(view.basicGrid)
+ .getColumn(BasicGridView.FIRST_NAME_KEY).getHeaderText());
+ Assertions.assertEquals("Age", test(view.basicGrid)
+ .getColumn(BasicGridView.AGE_KEY).getHeaderText());
+ Assertions.assertEquals("Subscriber", test(view.basicGrid)
+ .getColumn(BasicGridView.SUBSCRIBER_KEY).getHeaderText());
+ Assertions.assertEquals("Deceased", test(view.basicGrid)
+ .getColumn(BasicGridView.DECEASED_KEY).getHeaderText());
+ }
+
+ @Test
+ void basicGrid_multiselect() {
+ // This is not normally appropriate for a test, but we are testing
+ // features.
+ view.basicGrid.setSelectionMode(Grid.SelectionMode.MULTI);
+
+ test(view.basicGrid).clickRow(0);
+ Assertions.assertTrue(test(view.basicGrid).getSelected().isEmpty(),
+ "Multiselect doesn't select for row click!");
+
+ test(view.basicGrid).select(0);
+ test(view.basicGrid).select(1);
+ Assertions.assertSame(2, test(view.basicGrid).getSelected().size());
+ }
+
+ @Test
+ void basicGrid_multiselectAll() {
+ // This is not normally appropriate for a test, but we are testing
+ // features.
+ view.basicGrid.setSelectionMode(Grid.SelectionMode.MULTI);
+
+ test(view.basicGrid).selectAll();
+ Assertions.assertSame(2, test(view.basicGrid).getSelected().size());
+ }
+
+ @Test
+ void basicGrid_singleSelectThrowsForSelectAll() {
+ GridTester, Person> grid_ = test(view.basicGrid);
+ Assertions.assertThrows(IllegalStateException.class, grid_::selectAll,
+ "Select all should throw for single select");
+ }
+
+ @Test
+ void basicGrid_Hidden_getTextThrows() {
+ view.basicGrid.setVisible(false);
+
+ GridTester, Person> grid_ = test(view.basicGrid);
+
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> grid_.getHeaderCell(0),
+ "Header cell shouldn't be available for hidden grid");
+ Assertions.assertThrows(IllegalStateException.class,
+ () -> grid_.getCellText(0, 0),
+ "Cell content shouldn't be available for hidden grid");
+ }
+
+ @Test
+ void basicGrid_doubleClick() {
+ AtomicInteger doubleClicks = new AtomicInteger(0);
+ view.basicGrid.addItemDoubleClickListener(
+ event -> doubleClicks.incrementAndGet());
+
+ test(view.basicGrid).clickRow(0);
+ Assertions.assertEquals(0, doubleClicks.get(),
+ "Click should not generate a double click event");
+
+ test(view.basicGrid).doubleClickRow(0);
+ Assertions.assertEquals(1, doubleClicks.get(),
+ "Double click event should have fired");
+
+ }
+
+ @Test
+ void getCellComponent_columnByKey_canClickAButton() {
+ final Component cellComponent = test(view.basicGrid).getCellComponent(1,
+ BasicGridView.BUTTON_KEY);
+ Assertions.assertInstanceOf(Button.class, cellComponent);
+ var button = (Button) cellComponent;
+ test(button).click();
+ var notification = $(Notification.class).last();
+ Assertions.assertEquals("Clicked!", test(notification).getText());
+ }
+
+ @Test
+ void getCellComponent_columnByKey_returnsInstantiatedComponent() {
+ final Component cellComponent = test(view.basicGrid).getCellComponent(1,
+ BasicGridView.SUBSCRIBER_KEY);
+ Assertions.assertInstanceOf(CheckBox.class, cellComponent);
+ Assertions.assertFalse(((CheckBox) cellComponent).isChecked());
+ }
+
+ @Test
+ void getCellComponentByFaultyKey_throwsException() {
+ GridTester, Person> grid_ = test(view.basicGrid);
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> grid_.getCellComponent(1, "property"));
+ }
+
+ @Test
+ void getCellComponent_columnByPosition_returnsInstantiatedComponent() {
+ final Component cellComponent = test(view.basicGrid).getCellComponent(1,
+ 2);
+ Assertions.assertInstanceOf(CheckBox.class, cellComponent);
+ Assertions.assertFalse(((CheckBox) cellComponent).isChecked());
+ }
+
+ @Test
+ void getCellComponent_columnByPosition_stringColumnThrows() {
+ GridTester, Person> grid_ = test(view.basicGrid);
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> grid_.getCellComponent(1, 1));
+ }
+
+ @Test
+ void basicGrid_reorderColumns() {
+ List> columns = new ArrayList<>(
+ view.basicGrid.getColumns());
+ Collections.reverse(columns);
+ view.basicGrid.setColumnOrder(columns);
+
+ // person 1
+ Assertions.assertEquals("Jorma",
+ test(view.basicGrid).getCellText(0, 4));
+ Assertions.assertEquals("46", test(view.basicGrid).getCellText(0, 3));
+
+ // person 2
+ Assertions.assertEquals("Maya", test(view.basicGrid).getCellText(1, 4));
+ Assertions.assertEquals("18", test(view.basicGrid).getCellText(1, 3));
+ }
+
+ @Test
+ void basicGrid_toggleLitRenderedColumn() {
+ boolean deceased = test(view.basicGrid).getRow(0).getDeceased();
+
+ Assertions.assertEquals(deceased,
+ test(view.basicGrid).getLitRendererPropertyValue(0,
+ BasicGridView.DECEASED_KEY, "deceased", Boolean.class));
+
+ test(view.basicGrid).invokeLitRendererFunction(0,
+ BasicGridView.DECEASED_KEY, "onClick");
+
+ Assertions.assertEquals(!deceased,
+ test(view.basicGrid).getLitRendererPropertyValue(0,
+ BasicGridView.DECEASED_KEY, "deceased", Boolean.class));
+ }
+
+}
diff --git a/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridView.java b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridView.java
new file mode 100644
index 000000000..feadc8495
--- /dev/null
+++ b/browserless-test/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridView.java
@@ -0,0 +1,80 @@
+/**
+ * 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.component.button.Button;
+import com.vaadin.flow.component.notification.Notification;
+import com.vaadin.flow.data.renderer.ComponentRenderer;
+import com.vaadin.flow.data.renderer.LitRenderer;
+import com.vaadin.flow.router.Route;
+
+@Tag("div")
+@Route(value = "basic-grid", registerAtStartup = false)
+public class BasicGridView extends Component implements HasComponents {
+
+ static final String FIRST_NAME_KEY = "First Name";
+ static final String LAST_NAME_KEY = "Last Name";
+ static final String AGE_KEY = "Age";
+ static final String SUBSCRIBER_KEY = "Subscriber";
+ static final String DECEASED_KEY = "Deceased";
+ static final String BUTTON_KEY = "Button";
+
+ final Grid basicGrid;
+ final Person person1;
+ final Person person2;
+
+ public BasicGridView() {
+ basicGrid = new Grid<>();
+
+ basicGrid.addColumn(Person::getFirstName).setKey(FIRST_NAME_KEY)
+ .setHeader("First Name");
+ basicGrid.addColumn(Person::getLastName).setKey(LAST_NAME_KEY)
+ .setHeader("Last Name").setVisible(false);
+ basicGrid.addColumn(Person::getAge).setKey(AGE_KEY).setHeader("Age");
+ basicGrid
+ .addColumn(new ComponentRenderer<>(
+ person -> new CheckBox(person.isSubscriber())))
+ .setKey(SUBSCRIBER_KEY).setHeader("Subscriber");
+ basicGrid.addColumn(deceasedRenderer()).setKey(DECEASED_KEY)
+ .setHeader("Deceased");
+ basicGrid
+ .addComponentColumn(person -> new Button("Click",
+ e -> Notification.show("Clicked!")))
+ .setKey(BUTTON_KEY).setHeader("Button");
+
+ add(basicGrid);
+
+ person1 = Person.createTestPerson1();
+ person1.setDeceased(true);
+ person2 = Person.createTestPerson2();
+
+ basicGrid.setItems(person1, person2);
+ }
+
+ private LitRenderer deceasedRenderer() {
+ return LitRenderer. of(
+ "${item.deceased ? 'Yes' : 'No'} ")
+ .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