Test Multiple Users and Windows
- Setting Up the Application Context
- Creating Users and Windows
- Signals
- Authenticated Users with Spring Security
- Authenticated Users with Quarkus Security
- Keep Tests Independent
Use this guide when the behavior under test depends on more than one user or window.
The examples cover plain Java, Spring, and Quarkus application-context factories.
They do not provide the Weld/CDI wiring from the Java EE/CDI setup.
That single-user recipe does not establish separate CDI session contexts for multiple browserless users; the factories below are not a drop-in replacement for its base class.
Complete the Spring, plain Java, or Quarkus setup first.
The examples are patterns to adapt: CartView, CheckoutView, SharedCounterView, and ChatView represent your application views.
For security scenarios, reuse the access-control configuration from Test View Access Control.
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 initialization:
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());Use the window instance to navigate, find components, and perform actions.
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());Signals
The application context registers the test SignalEnvironment, so signal effects run deterministically instead of on a background thread pool. For single-user examples, see Test Signal-Based Views. When one window mutates a signal that other windows observe — the typical pattern for collaborative features built on shared signals — call runPendingSignalsTasks() to process the pending effects before asserting on the observing window:
Source code
Two Users Observing a Shared Signal
var w1 = app.newUser().newWindow();
w1.navigate(ChatView.class);
var w2 = app.newUser().newWindow();
w2.navigate(ChatView.class);
// w1 updates a shared signal that both views are bound to
w1.findTextField().withLabel("Message").setValue("Hello!");
w1.findButton().withText("Send").click();
// Process the pending signal effects, then assert on the other window
w2.runPendingSignalsTasks();
Assertions.assertEquals("Hello!", w2.findParagraph().getText());For background updates and write confirmation, see Test Signal-Based Views.
Authenticated Users with Spring Security
Create a secured application context, then interleave actions from an administrator and an anonymous user. Assert that the anonymous user is redirected while the administrator retains access.
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());
}
}For custom credentials, anonymous users, and logout behavior, see Spring Security Contexts.
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.
Keep Tests Independent
Close the application context after each test and reset application-owned shared data. Create and use each context on the same test thread. See context guarantees for thread affinity, direct API access, and security-state ownership.
B92B85CC-5CFD-4B22-8BA6-B61CC1903D7B