Multi-User and Multi-Window Testing
- When to Use This API
- Application, User, and Window
- Setting Up the Application Context
- Creating Users and Windows
- Authenticated Users with Spring Security
- Authenticated Users with Quarkus Security
- Customizing the Application Context
- Pitfalls and Guarantees
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 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 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 SpringBrowserlessTest, BrowserlessTest, QuarkusBrowserlessTest, or the JUnit 6 extensions.
Application, User, and Window
The API exposes three nested contexts that mirror Vaadin’s runtime hierarchy:
| One |
| One |
| One |
|
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 BrowserlessUIContext, the API automatically switches the thread-local Vaadin state — VaadinService, VaadinSession, 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 @BeforeEach, and closed in @AfterEach. Use try-with-resources or call close() explicitly: closing the application context cascades to every user and window it created.
create() accepts the packages that contain @Route-annotated views, either as package names or as classes whose packages should be scanned. Passing classes plays well with IDE refactoring and is the preferred form.
Source code
Plain Java
try (var app = BrowserlessApplicationContext.create(CartView.class)) {
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:
Source code
Spring
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ShopTestConfig.class)
class CartViewMultiUserTest {
@Autowired
private ApplicationContext applicationContext;
private BrowserlessApplicationContext app;
@BeforeEach
void setUp() {
app = SpringBrowserlessApplicationContext.create(applicationContext,
CartView.class);
}
@AfterEach
void tearDown() {
app.close();
}
}Source code
Quarkus
@QuarkusTest
class CartViewMultiUserTest {
private BrowserlessApplicationContext app;
@BeforeEach
void setUp() {
app = QuarkusBrowserlessApplicationContext.create(CartView.class);
}
@AfterEach
void tearDown() {
app.close();
}
}Creating Users and Windows
newUser() returns a fresh BrowserlessUserContext with its own VaadinSession. newWindow() creates a new UI for that user. Different users have independent sessions; different windows of the same user share a session but have independent UI instances.
Source code
Two Users, Independent Sessions
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 BrowserlessTest, scoped to that window — no explicit activation is required:
-
window.navigate(…)— navigate the window to a view. -
window.findButton(),window.findTextField(),window.findGrid(Class<V>), … — typed locator entry points that combine a filter chain with the component’s tester actions in one fluent expression; see Component Locators. -
window.find(Class)andwindow.findInView(Class)— locate components in this window. Returns aComponentQuery; see Querying Components. -
window.test(component)— wrap a component in a tester that simulates user actions likeclick()andsetValue(). Returns a typed tester matched to the component’s type; see Component Testers. -
window.getCurrentView()andwindow.roundTrip()— convenience accessors that mirror theirBrowserlessTestcounterparts.
Source code
Two Users Sharing Application-Level State
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.findButton().withText("Increment").click();
Assertions.assertEquals("Count: 1", w1.findParagraph().getText());
// w2 still shows its own UI state until it refreshes
Assertions.assertEquals("Count: 0", w2.findParagraph().getText());
w2.findButton().withText("Refresh").click();
Assertions.assertEquals("Count: 1", w2.findParagraph().getText());Source code
Same User, Two Windows, Independent UI State
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, SpringBrowserlessApplicationContext.createSecured() returns a SecuredBrowserlessApplicationContext<Authentication> that exposes credential-aware newUser(…) overloads. The handler installs the user’s Authentication on the calling thread before 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 SecurityContext is saved and the incoming user’s snapshot is restored automatically.
Source code
Multi-User Security Isolation
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SecurityTestConfig.class)
class MultiUserSecurityTest {
@Autowired
private ApplicationContext applicationContext;
private SecuredBrowserlessApplicationContext<Authentication> app;
@BeforeEach
void setUp() {
app = SpringBrowserlessApplicationContext.createSecured(
applicationContext, ProtectedView.class);
}
@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());
}
}newUser(String username, String… roles) is a convenience that produces an Authentication with the conventions of @WithMockUser. To install a custom Authentication directly, pass it to newUser(Authentication). Calling newUser() without arguments creates an anonymous user; the handler installs Spring’s AnonymousAuthenticationToken.
Authenticated Users with Quarkus Security
The Quarkus factory follows the same pattern with SecurityIdentity as the credential type:
Source code
Quarkus Multi-User Test
@QuarkusTest
@TestProfile(SecurityTestConfig.class)
class MultiUserSecurityTest {
private SecuredBrowserlessApplicationContext<SecurityIdentity> app;
@BeforeEach
void setUp() {
app = QuarkusBrowserlessApplicationContext
.createSecured(ProtectedView.class);
}
@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, newUser() without arguments creates an anonymous user, and cross-user window switches save and restore the active SecurityIdentity automatically.
Customizing the Application Context
For advanced setups, BrowserlessApplicationContext.Builder exposes the same configuration knobs as the framework-specific factories. Construct one directly and call build() for an unsecured context, or chain withSecurityContextHandler(…) to transition to a typed SecuredBrowserlessApplicationContext.Builder<C> whose build() returns the credential-aware variant.
| Method | Purpose |
|---|---|
| Adds packages to scan for |
| Adds packages to scan for custom |
| Enables credential-aware |
| Replaces the default mock servlet — for example, to plug in a framework-specific servlet. |
| Provides a custom |
| Registers Vaadin |
| Registers a callback to run after the context tears down — intended for releasing framework-specific state. |
For one-off tweaks without holding on to a builder reference, BrowserlessApplicationContext.create(UnaryOperator<Builder>) and the corresponding createSecured(Function<Builder, SecuredBuilder<C>>) accept a configurer:
Source code
Java
try (var app = BrowserlessApplicationContext.create(b -> b
.withViewPackages(CartView.class)
.withCloseHook(() -> testFixtures.reset()))) {
// ...
}The Spring and Quarkus factories expose 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
close()in@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
BrowserlessApplicationContextfrom 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
BrowserlessUIContextactivate the window automatically. If the test reaches forUI.getCurrent(),VaadinSession.getCurrent(), orSecurityContextHolderbetween DSL calls, callwindow.activate()first to make sure the thread-locals reflect the intended window. -
Anonymous users still go through the handler. On a secured context,
newUser()with no arguments delegates to the handler, which installs its anonymous-equivalent state (for example, Spring’sAnonymousAuthenticationToken).
9C4E8A2D-6F31-4B72-9A8F-5D7E1C3B4F60