From 5864df2a43375122672843d41fd1deadb676297c Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Thu, 23 Apr 2026 16:32:37 +0200 Subject: [PATCH 1/5] docs: document shared Vaadin env and JUnit 6 extensions Browserless testing in Vaadin 25.2 adds a per-class lifecycle to `BrowserlessTest` and introduces `BrowserlessExtension` and `BrowserlessClassExtension` as composition-based alternatives to extending a base class. The existing documentation only covered the inheritance-based, per-method setup. Add a "Sharing the Vaadin Environment Across Tests" section to `optimizing-tests.adoc` covering `@TestInstance(PER_CLASS)` with `BrowserlessTest`, including a warning about shared state and a note that `SpringBrowserlessTest` and `QuarkusBrowserlessTest` still reinitialize per method. Add a new `extensions.adoc` page documenting `BrowserlessExtension` (per-method) and `BrowserlessClassExtension` (per-class), the builder API (`withViewPackages`, `withServices`, `withComponentTesterPackages`), and how they relate to the base class approach. Add a short tip in `non-spring.adoc` pointing projects with an existing test base class to the new extensions page. Part of https://github.com/vaadin/browserless-test/issues/22 --- .../flow/testing/browserless/extensions.adoc | 130 ++++++++++++++++++ .../flow/testing/browserless/non-spring.adoc | 4 + .../testing/browserless/optimizing-tests.adoc | 40 ++++++ 3 files changed, 174 insertions(+) create mode 100644 articles/flow/testing/browserless/extensions.adoc diff --git a/articles/flow/testing/browserless/extensions.adoc b/articles/flow/testing/browserless/extensions.adoc new file mode 100644 index 0000000000..6abb5624ac --- /dev/null +++ b/articles/flow/testing/browserless/extensions.adoc @@ -0,0 +1,130 @@ +--- +title: JUnit 6 Extensions +page-title: Composition-based browserless testing with JUnit 6 extensions +description: Use BrowserlessExtension and BrowserlessClassExtension as an alternative to extending BrowserlessTest. +meta-description: Set up Vaadin browserless tests with JUnit 6 @RegisterExtension when extending a base class is not an option. +order: 45 +--- + + += JUnit 6 Extensions + +[since:com.vaadin:vaadin@V25.2] + +Extending [classname]`BrowserlessTest` is the most compact way to write browserless tests, but it requires your test class to use inheritance for the Vaadin setup. When a project already has its own test base class, or you prefer composition over inheritance, the [classname]`BrowserlessExtension` and [classname]`BrowserlessClassExtension` JUnit 6 extensions provide the same functionality without requiring a specific superclass. + +[NOTE] +These extensions are part of the `browserless-test-junit6` artifact. They don't replace [classname]`SpringBrowserlessTest` or [classname]`QuarkusBrowserlessTest` -- for Spring and Quarkus projects, continue to extend those base classes. + + +== When to Use an Extension + +Use an extension when any of the following applies: + +- The test class already extends another base class that can't be changed. +- The project standardizes on composition-based JUnit 6 extensions. + + +== Per-Method Lifecycle + +[classname]`BrowserlessExtension` creates a fresh Vaadin environment before each test method and tears it down after. Register it as an instance field with [annotationname]`@RegisterExtension`: + +[source,java] +---- +@ViewPackages(classes = CartView.class) +class CartViewTest { + + @RegisterExtension + BrowserlessExtension ext = new BrowserlessExtension(); + + @Test + void addItemToCart() { + CartView view = ext.navigate(CartView.class); + ext.test(view.getAddButton()).click(); + + Assertions.assertEquals(1, view.getCartSize()); + } +} +---- + +Navigation, queries, and tester interactions are available as methods on the extension instance -- [methodname]`ext.navigate()`, [methodname]`ext.$()`, [methodname]`ext.$view()`, [methodname]`ext.test()`, [methodname]`ext.getCurrentView()`, [methodname]`ext.fireShortcut()`, [methodname]`ext.roundTrip()`, and [methodname]`ext.runPendingSignalsTasks()`. + + +== Per-Class Lifecycle + +[classname]`BrowserlessClassExtension` initializes the Vaadin environment once in [annotationname]`@BeforeAll` and shares it across all tests in the class. Register it as a `static` field: + +[source,java] +---- +@ViewPackages(classes = CartView.class) +class CartViewSharedTest { + + @RegisterExtension + static BrowserlessClassExtension ext = new BrowserlessClassExtension(); + + @BeforeAll + static void setup() { + ext.navigate(CartView.class); + } + + @Test + void addItem() { + // same UI instance as removeItem + ext.test(ext.$(Button.class).withText("Add").single()).click(); + } + + @Test + void removeItem() { + // state from addItem is preserved + } +} +---- + +The same trade-offs as <> apply: state leaks between tests, so write tests that tolerate leftover state or reset it explicitly. + + +== Configuring the Extension + +Both extensions support a builder-style API for configuration, used as an alternative or in addition to annotations. + +.Builder API +[cols="1,2"] +|=== +| Method | Description + +| [methodname]`withViewPackages(Class...)` +| Adds the packages of the given classes to the route scan. Equivalent to [annotationname]`@ViewPackages(classes = ...)`. + +| [methodname]`withViewPackages(String...)` +| Adds package names (as strings) to the route scan. Equivalent to [annotationname]`@ViewPackages(packages = ...)`. + +| [methodname]`withServices(Class...)` +| Registers custom implementations with the Vaadin [classname]`Lookup` SPI. + +| [methodname]`withComponentTesterPackages(String...)` +| Adds packages to scan for custom [classname]`ComponentTester` implementations. Equivalent to [annotationname]`@ComponentTesterPackages`. +|=== + +The [annotationname]`@ViewPackages` annotation still works when placed on the test class; programmatic configuration adds to what the annotation declares. + +.Extension with Programmatic Configuration +[source,java] +---- +class AdminViewTest { + + @RegisterExtension + BrowserlessExtension ext = new BrowserlessExtension() + .withViewPackages("com.example.views", "com.example.admin") + .withServices(CustomInstantiatorFactory.class) + .withComponentTesterPackages("com.example.testers"); + + @Test + void adminDashboardLoads() { + AdminDashboardView view = ext.navigate(AdminDashboardView.class); + Assertions.assertNotNull(view); + } +} +---- + + +[discussion-id]`B51F9D4A-2E73-4B18-8C6F-9A3D7E2B1C04` diff --git a/articles/flow/testing/browserless/non-spring.adoc b/articles/flow/testing/browserless/non-spring.adoc index 8c16203bf9..166371ffaa 100644 --- a/articles/flow/testing/browserless/non-spring.adoc +++ b/articles/flow/testing/browserless/non-spring.adoc @@ -52,6 +52,10 @@ class HelloWorldViewTest extends BrowserlessTest { All the features described in the <> guide — package scanning, navigation, component testing, and component queries — work the same way with [classname]`BrowserlessTest`. Replace [classname]`SpringBrowserlessTest` with [classname]`BrowserlessTest` and remove the [annotationname]`@SpringBootTest` annotation. +.Already Have a Test Base Class? +[TIP] +If your project already has its own test base class, you can still get the Vaadin environment without extending [classname]`BrowserlessTest`. See <> for the composition-based setup. + == Running Tests diff --git a/articles/flow/testing/browserless/optimizing-tests.adoc b/articles/flow/testing/browserless/optimizing-tests.adoc index a7a151af83..16ead71cad 100644 --- a/articles/flow/testing/browserless/optimizing-tests.adoc +++ b/articles/flow/testing/browserless/optimizing-tests.adoc @@ -81,4 +81,44 @@ class ViewTestConfig { ---- +== Sharing the Vaadin Environment Across Tests + +[since:com.vaadin:vaadin@V25.2] + +By default, the Vaadin environment -- the session, the UI, and all routes -- is created before every test method and torn down after. For classes with many tests that navigate to views sharing the same [classname]`MainLayout`, this setup cost can dominate the test runtime. + +Annotating the test class with [annotationname]`@TestInstance(Lifecycle.PER_CLASS)` initializes the environment once in [annotationname]`@BeforeAll` and shares the same [classname]`UI` instance across all test methods in the class. This can significantly reduce runtime for suites with hundreds of tests on the same view. + +.Per-Class Lifecycle Example +[source,java] +---- +@ViewPackages(classes = CartView.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CartViewTest extends BrowserlessTest { + + @BeforeAll + void setup() { + navigate(CartView.class); + } + + @Test + void addItem_increasesCartSize() { + // same UI instance as the other tests + test($(Button.class).withText("Add").single()).click(); + } + + @Test + void removeItem_decreasesCartSize() { + // state from the previous test is preserved + } +} +---- + +[WARNING] +With a shared environment, state leaks between tests. Tests must either tolerate leftover state or reset it explicitly -- for example, by re-navigating to the view in a [annotationname]`@BeforeEach` method. Prefer per-class lifecycle for read-only or independent interactions; stick with the default per-method lifecycle when tests mutate shared state in conflicting ways. + +[NOTE] +This optimization applies only to [classname]`BrowserlessTest`. [classname]`SpringBrowserlessTest` and [classname]`QuarkusBrowserlessTest` still reinitialize the Vaadin environment before each test method, so [annotationname]`@TestInstance(PER_CLASS)` on those classes doesn't share the Vaadin environment -- though it can still be useful for sharing other per-class state. + + [discussion-id]`A3B7E2F1-5D89-4C6A-9E12-7F4A8B3C6D50` From 63ca1b4c7256338a98eb59710d7a14cc1cbd58a2 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Mon, 11 May 2026 15:26:31 +0200 Subject: [PATCH 2/5] docs: document multi-user and multi-window testing API Vaadin 25.2 introduces `BrowserlessApplicationContext`, a composition- based API for browserless tests that need multiple concurrent user sessions or multiple windows per user. The existing pages only cover the single-user, single-window scenario provided by `BrowserlessTest` and the JUnit 6 extensions. Add a new `multi-user.adoc` page covering setup, creating users and windows, authenticated users with Spring Security and Quarkus Security, and the main pitfalls. Add a uniform "Multiple Users or Windows?" tip on the existing browserless pages pointing readers to the new page. Part of https://github.com/vaadin/browserless-test/issues/22 --- .../testing/browserless/getting-started.adoc | 5 + .../flow/testing/browserless/multi-user.adoc | 341 ++++++++++++++++++ .../flow/testing/browserless/non-spring.adoc | 4 + .../flow/testing/browserless/quarkus.adoc | 6 + .../testing/browserless/spring-security.adoc | 5 + 5 files changed, 361 insertions(+) create mode 100644 articles/flow/testing/browserless/multi-user.adoc diff --git a/articles/flow/testing/browserless/getting-started.adoc b/articles/flow/testing/browserless/getting-started.adoc index 8b236c69b1..d9d040920d 100644 --- a/articles/flow/testing/browserless/getting-started.adoc +++ b/articles/flow/testing/browserless/getting-started.adoc @@ -215,4 +215,9 @@ fireShortcut(Key.KEY_S, KeyModifier.CONTROL); ---- +.Multiple Users or Windows? +[TIP] +This setup drives a single user with a single window. For tests that need several concurrent users or multiple windows of the same user, see <>. + + [discussion-id]`7F423DA0-1C41-44BA-B832-55C269FA9311` diff --git a/articles/flow/testing/browserless/multi-user.adoc b/articles/flow/testing/browserless/multi-user.adoc new file mode 100644 index 0000000000..05e9fea8f1 --- /dev/null +++ b/articles/flow/testing/browserless/multi-user.adoc @@ -0,0 +1,341 @@ +--- +title: Multi-User and Multi-Window Testing +page-title: Browserless testing for multi-user and multi-window scenarios in Vaadin +description: Drive multiple concurrent user sessions and multiple browser windows in a single browserless test. +meta-description: Test Vaadin features that depend on multiple users or multiple windows -- shared state, security isolation, and per-window UI state -- without a browser. +order: 32 +--- + + += Multi-User and Multi-Window Testing + +[since:com.vaadin:vaadin@V25.2] + +[classname]`BrowserlessTest` and the JUnit 6 extensions cover the most common scenario: one user, one window. Some features can only be exercised with multiple participants in the same test -- application-wide singletons observed by two users, per-window UI state of the same user, or security context isolation when switching between authenticated users. + +The [classname]`BrowserlessApplicationContext` API models this directly. It is a composition-based, lower-level alternative that lets the test create as many user sessions and UI instances as needed, then drive them independently. + + +== When to Use This API + +Reach for [classname]`BrowserlessApplicationContext` when: + +- Two or more users must be active at the same time -- for example, asserting that a shared service exposes the same state to both, or that one user's mutation does not leak into another user's session. +- A single user has multiple browser windows open and per-window UI state must remain isolated while session state is shared. +- A test switches between authenticated users and needs the Spring Security or Quarkus security context to follow the active window. + +For tests with a single user and a single window, prefer <>, <>, <>, or the <>. + + +== Application, User, and Window + +The API exposes three nested contexts that mirror Vaadin's runtime hierarchy: + +[cols="1,3"] +|=== +| [classname]`BrowserlessApplicationContext` +| One [classname]`VaadinService` and one servlet, shared across every user and window the test creates. + +| [classname]`BrowserlessUserContext` +| One [classname]`VaadinSession` and the security state of a single logical user. A user can own many windows. + +| [classname]`BrowserlessUIContext` +| One [classname]`UI` instance -- a single browser window. Provides the testing DSL ([methodname]`navigate`, [methodname]`$`, [methodname]`test`, ...). +|=== + +[NOTE] +All three contexts are *thread-affine*: they must be created, used, and closed on the same thread. Driving the same context from parallel test threads is not supported. + +When the test calls a DSL method on any [classname]`BrowserlessUIContext`, the API automatically switches the thread-local Vaadin state -- [classname]`VaadinService`, [classname]`VaadinSession`, [classname]`UI`, request, response, and the security context -- to that window's user. Interleaving calls on different windows is therefore safe without any explicit context switch. + + +== Setting Up the Application Context + +The application context is built once per test, typically in [annotationname]`@BeforeEach`, and closed in [annotationname]`@AfterEach`. Use try-with-resources or call [methodname]`close()` explicitly: closing the application context cascades to every user and window it created. + +Routes are discovered with [classname]`Routes.autoDiscoverViews(...)`, passing the packages that contain [annotationname]`@Route`-annotated views. + +.Plain Java +[source,java] +---- +Routes routes = new Routes() + .autoDiscoverViews("com.example.shop"); + +try (var app = BrowserlessApplicationContext.create(routes)) { + var user = app.newUser(); + var window = user.newWindow(); + window.navigate(CartView.class); + // assertions... +} +---- + +For Spring and Quarkus, dedicated factories pre-wire the framework-specific servlet and lookup initializer: + +.Spring +[source,java] +---- +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ShopTestConfig.class) +class CartViewMultiUserTest { + + @Autowired + private ApplicationContext applicationContext; + + private BrowserlessApplicationContext app; + + @BeforeEach + void setUp() { + Routes routes = new Routes() + .autoDiscoverViews("com.example.shop"); + app = SpringBrowserlessApplicationContext.create(routes, + applicationContext); + } + + @AfterEach + void tearDown() { + app.close(); + } +} +---- + +.Quarkus +[source,java] +---- +@QuarkusTest +class CartViewMultiUserTest { + + private BrowserlessApplicationContext app; + + @BeforeEach + void setUp() { + Routes routes = new Routes() + .autoDiscoverViews("com.example.shop"); + app = QuarkusBrowserlessApplicationContext.create(routes); + } + + @AfterEach + void tearDown() { + app.close(); + } +} +---- + + +== Creating Users and Windows + +[methodname]`newUser()` returns a fresh [classname]`BrowserlessUserContext` with its own [classname]`VaadinSession`. [methodname]`newWindow()` creates a new [classname]`UI` for that user. Different users have independent sessions; different windows of the same user share a session but have independent [classname]`UI` instances. + +.Two Users, Independent Sessions +[source,java] +---- +var alice = app.newUser(); +var aliceWindow = alice.newWindow(); + +var bob = app.newUser(); +var bobWindow = bob.newWindow(); + +Assertions.assertNotSame(alice.getSession(), bob.getSession()); +Assertions.assertNotSame(aliceWindow.getUI(), bobWindow.getUI()); +---- + +Every window exposes the same testing DSL as [classname]`BrowserlessTest`, scoped to that window -- no explicit activation is required: + +- [methodname]`window.navigate(...)` -- navigate the window to a view. +- [methodname]`window.$(Class)` and [methodname]`window.$view(Class)` -- locate components in this window. Returns a [classname]`ComponentQuery`; see <>. +- [methodname]`window.test(component)` -- wrap a component in a tester that simulates user actions like [methodname]`click()` and [methodname]`setValue()`. Returns a typed tester matched to the component's type; see <>. +- [methodname]`window.getCurrentView()` and [methodname]`window.roundTrip()` -- convenience accessors that mirror their [classname]`BrowserlessTest` counterparts. + +.Two Users Sharing Application-Level State +[source,java] +---- +var w1 = app.newUser().newWindow(); +w1.navigate(SharedCounterView.class); + +var w2 = app.newUser().newWindow(); +w2.navigate(SharedCounterView.class); + +// w1 mutates a shared static counter +w1.test(w1.$(Button.class).withText("Increment").single()).click(); +Assertions.assertEquals("Count: 1", + w1.$(Paragraph.class).single().getText()); + +// w2 still shows its own UI state until it refreshes +Assertions.assertEquals("Count: 0", + w2.$(Paragraph.class).single().getText()); + +w2.test(w2.$(Button.class).withText("Refresh").single()).click(); +Assertions.assertEquals("Count: 1", + w2.$(Paragraph.class).single().getText()); +---- + +.Same User, Two Windows, Independent UI State +[source,java] +---- +var user = app.newUser(); +var w1 = user.newWindow(); +var w2 = user.newWindow(); + +w1.navigate(CartView.class); +w2.navigate(CheckoutView.class); + +// Each window holds its own current view +Assertions.assertInstanceOf(CartView.class, w1.getCurrentView()); +Assertions.assertInstanceOf(CheckoutView.class, w2.getCurrentView()); + +// Session is the same; UIs are not +Assertions.assertSame(user.getSession(), w1.getUI().getSession()); +Assertions.assertNotSame(w1.getUI(), w2.getUI()); +---- + + +== Authenticated Users with Spring Security + +When Spring Security is on the classpath, [methodname]`SpringBrowserlessApplicationContext.createSecured()` returns a [classname]`SecuredBrowserlessApplicationContext` that exposes credential-aware [methodname]`newUser(...)` overloads. The handler installs the user's [classname]`Authentication` on the calling thread before [classname]`SessionInit` listeners fire, mirroring the order of operations in a real Vaadin + Spring Security request. + +When the test switches between windows belonging to different users, the outgoing user's [classname]`SecurityContext` is saved and the incoming user's snapshot is restored automatically. + +.Multi-User Security Isolation +[source,java] +---- +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = SecurityTestConfig.class) +class MultiUserSecurityTest { + + @Autowired + private ApplicationContext applicationContext; + + private SecuredBrowserlessApplicationContext app; + + @BeforeEach + void setUp() { + Routes routes = new Routes() + .autoDiscoverViews("com.example.security"); + app = SpringBrowserlessApplicationContext.createSecured(routes, + applicationContext); + } + + @AfterEach + void tearDown() { + app.close(); + } + + @Test + void switchingUsers_securityContextFollowsActiveWindow() { + var admin = app.newUser("john", "ADMIN").newWindow(); + var anon = app.newUser().newWindow(); + + admin.navigate(ProtectedView.class); + Assertions.assertInstanceOf(ProtectedView.class, + admin.getCurrentView()); + + // Switching to the anonymous user restores their (empty) context; + // the protected view redirects to login. + Assertions.assertThrows(IllegalArgumentException.class, + () -> anon.navigate(ProtectedView.class)); + Assertions.assertInstanceOf(LoginView.class, anon.getCurrentView()); + + // Switching back restores admin's authentication. + admin.navigate(ProtectedView.class); + Assertions.assertInstanceOf(ProtectedView.class, + admin.getCurrentView()); + } +} +---- + +[methodname]`newUser(String username, String... roles)` is a convenience that produces an [classname]`Authentication` with the conventions of [annotationname]`@WithMockUser`. To install a custom [classname]`Authentication` directly, pass it to [methodname]`newUser(Authentication)`. Calling [methodname]`newUser()` without arguments creates an anonymous user; the handler installs Spring's [classname]`AnonymousAuthenticationToken`. + + +== Authenticated Users with Quarkus Security + +The Quarkus factory follows the same pattern with [classname]`SecurityIdentity` as the credential type: + +.Quarkus Multi-User Test +[source,java] +---- +@QuarkusTest +@TestProfile(SecurityTestConfig.class) +class MultiUserSecurityTest { + + private SecuredBrowserlessApplicationContext app; + + @BeforeEach + void setUp() { + Routes routes = new Routes() + .autoDiscoverViews("com.example.security"); + app = QuarkusBrowserlessApplicationContext.createSecured(routes); + } + + @AfterEach + void tearDown() { + app.close(); + } + + @Test + void authenticatedUser_byUsernameAndRoles_seesProtectedView() { + var window = app.newUser("john", "USER").newWindow(); + + window.navigate(ProtectedView.class); + Assertions.assertInstanceOf(ProtectedView.class, + window.getCurrentView()); + } + + @Test + void authenticatedUser_byIdentity_seesProtectedView() { + SecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal("john")) + .addRoles(Set.of("USER")) + .setAnonymous(false) + .build(); + + var window = app.newUser(identity).newWindow(); + + window.navigate(ProtectedView.class); + Assertions.assertInstanceOf(ProtectedView.class, + window.getCurrentView()); + } +} +---- + +As with the Spring factory, [methodname]`newUser()` without arguments creates an anonymous user, and cross-user window switches save and restore the active [classname]`SecurityIdentity` automatically. + + +== Customizing the Application Context + +[methodname]`BrowserlessApplicationContext.builder(routes)` exposes the same configuration knobs as the framework-specific factories. Call [methodname]`build()` directly for an unsecured context, or chain [methodname]`withSecurityContextHandler(...)` to transition to a typed [classname]`SecuredBrowserlessApplicationContext.Builder` whose [methodname]`build()` returns the credential-aware variant. + +.Builder Configuration +[cols="1,2"] +|=== +| Method | Purpose + +| [methodname]`withSecurityContextHandler(SecurityContextHandler)` +| Enables credential-aware [methodname]`newUser(...)` overloads. Transitions to the typed [classname]`SecuredBrowserlessApplicationContext.Builder`. + +| [methodname]`withServletFactory(BiFunction)` +| Replaces the default mock servlet -- for example, to plug in a framework-specific servlet. + +| [methodname]`withUIFactory(UIFactory)` +| Provides a custom [classname]`UI` subclass for every window created by the context. + +| [methodname]`withLookupServices(Class...)` +| Registers Vaadin [classname]`Lookup` service implementations. + +| [methodname]`withCloseHook(Runnable)` +| Registers a callback to run after the context tears down -- intended for releasing framework-specific state. +|=== + +The Spring and Quarkus factories expose [methodname]`builder(...)` overloads that return a pre-wired builder for projects that need to add further customizations on top of the framework defaults. + + +== Pitfalls and Guarantees + +Common gotchas worth keeping in mind: + +- *Always close the application context.* Use try-with-resources or call [methodname]`close()` in [annotationname]`@AfterEach`. Closing the application cascades to every user and window. Leaking a context across tests leaves Vaadin thread-locals pointing at torn-down state. +- *No parallel access.* Every context is thread-affine. Driving the same [classname]`BrowserlessApplicationContext` from multiple threads in the same test is unsupported. +- *Security snapshot is per user, not per window.* Two windows of the same user share one snapshot; a security mutation made while one window is active is visible to the user's other windows. The snapshot is re-captured only on cross-user switches. +- *Calling Vaadin APIs directly.* DSL methods on [classname]`BrowserlessUIContext` activate the window automatically. If the test reaches for [methodname]`UI.getCurrent()`, [methodname]`VaadinSession.getCurrent()`, or [classname]`SecurityContextHolder` between DSL calls, call [methodname]`window.activate()` first to make sure the thread-locals reflect the intended window. +- *Anonymous users still go through the handler.* On a secured context, [methodname]`newUser()` with no arguments delegates to the handler, which installs its anonymous-equivalent state (for example, Spring's [classname]`AnonymousAuthenticationToken`). + + +[discussion-id]`9C4E8A2D-6F31-4B72-9A8F-5D7E1C3B4F60` diff --git a/articles/flow/testing/browserless/non-spring.adoc b/articles/flow/testing/browserless/non-spring.adoc index 166371ffaa..e9fd3d06b8 100644 --- a/articles/flow/testing/browserless/non-spring.adoc +++ b/articles/flow/testing/browserless/non-spring.adoc @@ -56,6 +56,10 @@ All the features described in the <> guide [TIP] If your project already has its own test base class, you can still get the Vaadin environment without extending [classname]`BrowserlessTest`. See <> for the composition-based setup. +.Multiple Users or Windows? +[TIP] +This setup drives a single user with a single window. For tests that need several concurrent users or multiple windows of the same user, see <>. + == Running Tests diff --git a/articles/flow/testing/browserless/quarkus.adoc b/articles/flow/testing/browserless/quarkus.adoc index cbe2831579..771509cde6 100644 --- a/articles/flow/testing/browserless/quarkus.adoc +++ b/articles/flow/testing/browserless/quarkus.adoc @@ -174,4 +174,10 @@ class ViewSecurityTest extends QuarkusBrowserlessTest { <1> Sets a profile to activate Vaadin access control feature. <2> Uses Quarkus test security annotations. + +.Multiple Users or Windows? +[TIP] +This setup drives a single user with a single window. For tests that need several concurrent users or multiple windows of the same user, see <>. + + [discussion-id]`61B2F8E5-448E-4C36-82E3-D492712ECE67` diff --git a/articles/flow/testing/browserless/spring-security.adoc b/articles/flow/testing/browserless/spring-security.adoc index 429c46b46e..e1f3312f97 100644 --- a/articles/flow/testing/browserless/spring-security.adoc +++ b/articles/flow/testing/browserless/spring-security.adoc @@ -169,4 +169,9 @@ class SecurityTestConfig { ---- +.Multiple Users or Windows? +[TIP] +This setup drives a single user with a single window. For tests that need several concurrent users or multiple windows of the same user, see <>. + + [discussion-id]`2A8B3F1E-9C47-4D5A-B621-8E3F2A7C9D10` From a356da6c66aa1ae62d658a8126c1647f8fc34716 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Fri, 15 May 2026 17:24:47 +0200 Subject: [PATCH 3/5] docs: rename `$()`/`$view()` to `find`/`findInView` Browserless-test 1.1 deprecates `$()` and `$view()` on `BaseBrowserlessTest` in favor of `find` and `findInView`, and the new composition-based APIs (`BrowserlessExtension`, `BrowserlessUIContext`) only expose the new names. Update all browserless docs and examples to use `find` / `findInView`. The reference page and the migration guide keep a short note pointing at the deprecated aliases for users on older versions. --- .../testing/browserless/component-query.adoc | 33 ++++++++++--------- .../flow/testing/browserless/extensions.adoc | 4 +-- .../testing/browserless/getting-started.adoc | 6 ++-- .../flow/testing/browserless/migration.adoc | 2 +- .../flow/testing/browserless/multi-user.adoc | 12 +++---- .../flow/testing/browserless/non-spring.adoc | 2 +- .../testing/browserless/optimizing-tests.adoc | 4 +-- .../browserless/overlay-components.adoc | 8 ++--- .../flow/testing/browserless/quarkus.adoc | 6 ++-- .../flow/testing/browserless/snapshots.adoc | 8 ++--- .../testing/browserless/spring-security.adoc | 6 ++-- 11 files changed, 47 insertions(+), 44 deletions(-) diff --git a/articles/flow/testing/browserless/component-query.adoc b/articles/flow/testing/browserless/component-query.adoc index 07f28708f2..137d312abc 100644 --- a/articles/flow/testing/browserless/component-query.adoc +++ b/articles/flow/testing/browserless/component-query.adoc @@ -16,28 +16,31 @@ To overcome this limitation, [classname]`BrowserlessTest` provides a component q == Component Queries -You can get a [classname]`ComponentQuery` object by calling the [methodname]`$()` method, specifying the type of the component you are searching for. +You can get a [classname]`ComponentQuery` object by calling the [methodname]`find()` method, specifying the type of the component you are searching for. Once the query is ready with all conditions configured, use a terminal operator to retrieve the components that it found. Examples of terminal operators are [methodname]`single()`, [methodname]`last()`, [methodname]`atIndex()`, [methodname]`all()`, and [methodname]`id()`. [source,java] ---- // Get the TextField -TextField nameField = $(TextField.class).single(); +TextField nameField = find(TextField.class).single(); ---- +[NOTE] +[classname]`BrowserlessTest` retains [methodname]`$()` and [methodname]`$view()` as deprecated aliases for [methodname]`find()` and [methodname]`findInView()`, scheduled for removal in 2.0. The newer composition-based APIs ([classname]`BrowserlessExtension`, [classname]`BrowserlessClassExtension`, and [classname]`BrowserlessUIContext`) only expose [methodname]`find()` and [methodname]`findInView()`. + === Scoping Queries -You can also restrict search scope to the children of the current view by using the [methodname]`$view()` method, or even to another component by using [methodname]`$(MyComponent.class, rootComponent)`. +You can also restrict search scope to the children of the current view by using the [methodname]`findInView()` method, or even to another component by using [methodname]`find(MyComponent.class, rootComponent)`. [source,java] ---- // Get the TextField in the current view -TextField nameField = $view(TextField.class).single(); +TextField nameField = findInView(TextField.class).single(); // Get the TextField nested in a container -TextField nameField = $(TextField.class, view.formLayout).single(); +TextField nameField = find(TextField.class, view.formLayout).single(); ---- The query object has many filtering methods that can be used to refine the search. @@ -45,14 +48,14 @@ The query object has many filtering methods that can be used to refine the searc [source,java] ---- // Get the TextField with the given label -TextField nameField = $view(TextField.class) +TextField nameField = findInView(TextField.class) .withPropertyValue(TextField::getLabel, "First name") .single(); // Get all TextFields in the view that satisfy the conditions Predicate fieldHasNotValue = field -> field.getOptionalValue().isEmpty(); Predicate fieldIsInvalid = TextField::isInvalid; -List textField = $view(TextField.class) +List textField = findInView(TextField.class) .withCondition(fieldHasNotValue.or(fieldIsInvalid)) .all(); ---- @@ -127,13 +130,13 @@ Here are a few examples: [source,java] ---- // Find a button by its text -Button save = $(Button.class).withText("Save").single(); +Button save = find(Button.class).withText("Save").single(); // Find a TextField by CSS class -TextField styled = $(TextField.class).withClassName("highlighted").single(); +TextField styled = find(TextField.class).withClassName("highlighted").single(); // Find checkboxes with a specific value -Checkbox checked = $(Checkbox.class).withValue(true).single(); +Checkbox checked = find(Checkbox.class).withValue(true).single(); ---- @@ -143,7 +146,7 @@ Use [methodname]`exists()` to check whether a query has results without throwing [source,java] ---- -if ($(Notification.class).exists()) { +if (find(Notification.class).exists()) { // A notification is open } ---- @@ -156,13 +159,13 @@ You can assert the number of results directly in the query chain: [source,java] ---- // Expect exactly 3 text fields -List fields = $(TextField.class).withResultsSize(3).all(); +List fields = find(TextField.class).withResultsSize(3).all(); // Expect between 1 and 5 results -List