Docs

Multi-User and Multi-Window Testing

Drive multiple concurrent user sessions and multiple browser windows in a single browserless test.

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:

BrowserlessApplicationContext

One VaadinService and one servlet, shared across every user and window the test creates.

BrowserlessUserContext

One VaadinSession and the security state of a single logical user. A user can own many windows.

BrowserlessUIContext

One UI instance — a single browser window. Provides the testing DSL (navigate, find, 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 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) and window.findInView(Class) — locate components in this window. Returns a ComponentQuery; see Querying Components.

  • window.test(component) — wrap a component in a tester that simulates user actions like click() and setValue(). Returns a typed tester matched to the component’s type; see Component Testers.

  • window.getCurrentView() and window.roundTrip() — convenience accessors that mirror their BrowserlessTest counterparts.

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.

Table 1. Builder Configuration
Method Purpose

withViewPackages(String…​) / withViewPackages(Class<?>…​)

Adds packages to scan for @Route-annotated views. Successive calls accumulate.

withComponentTesterPackages(String…​) / withComponentTesterPackages(Class<?>…​)

Adds packages to scan for custom ComponentTester implementations.

withSecurityContextHandler(SecurityContextHandler<C>)

Enables credential-aware newUser(…​) overloads. Transitions to the typed SecuredBrowserlessApplicationContext.Builder<C>.

withServletFactory(…​)

Replaces the default mock servlet — for example, to plug in a framework-specific servlet.

withUIFactory(UIFactory)

Provides a custom UI subclass for every window created by the context.

withLookupServices(Class<?>…​)

Registers Vaadin Lookup service implementations.

withCloseHook(Runnable)

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 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 BrowserlessUIContext activate the window automatically. If the test reaches for UI.getCurrent(), VaadinSession.getCurrent(), or SecurityContextHolder between DSL calls, call window.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’s AnonymousAuthenticationToken).

9C4E8A2D-6F31-4B72-9A8F-5D7E1C3B4F60

Updated