Summary
When using the 1.1 multi-user API (SpringBrowserlessApplicationContext →
newUser() → newWindow()), each user gets its own VaadinSession, but
Spring-managed session-scoped beans (@SessionScope and, by extension,
@VaadinSessionScope) are not resolved per user. The second user reuses the
first user's session-scoped bean instance. When such a bean holds a local
ValueSignal (or ListSignal), the second user's UI then accesses a signal that
was created in the first user's VaadinSession, which fails with:
java.lang.IllegalStateException: This ValueSignal instance was created in one
VaadinSession but is being accessed from another. This typically happens when a
local signal is stored in a static field or an application-scoped bean. Use
SharedValueSignal or SharedListSignal instead for state that is shared across
sessions.
at com.vaadin.flow.signals.local.AbstractLocalSignal.checkPreconditions(AbstractLocalSignal.java:71)
This blocks multi-user testing for any app whose views (or shared parent layout)
depend on a Spring session-scoped bean — a very common pattern.
Environment
- browserless-test: 1.1.x (Vaadin platform 25.2)
- Vaadin Flow: 25.2
- Spring Boot: 4.1.0
- Java: 25
Expected behavior
Each BrowserlessUserContext created via newUser(...) should resolve Spring
session-scoped beans (@SessionScope, @VaadinSessionScope) against that user's
own session, so two users get independent instances — mirroring two real browser
sessions. A signal stored in a @SessionScope bean should be per-user.
Actual behavior
The second user reuses the first user's session-scoped bean instance. The bean's
local ValueSignal belongs to the first user's VaadinSession, so binding it in
the second user's UI throws the cross-session IllegalStateException above and
navigation fails.
Key diagnostic
- Single user, single (or multiple) window through the same app context: works.
A @SessionScope bean with a local ValueSignal, bound in a view, renders
fine for one user.
- Two users: the second fails on the cross-session signal access.
This pins the gap to per-user session-scope isolation, not to the API surface,
signals support, or navigation in general.
Minimal reproduction
// A Spring session-scoped bean holding a per-session local signal.
@org.springframework.stereotype.Component
@org.springframework.web.context.annotation.SessionScope
class Prefs {
final com.vaadin.flow.signals.local.ValueSignal<String> color =
new com.vaadin.flow.signals.local.ValueSignal<>("white");
}
@Route("repro")
class ReproView extends com.vaadin.flow.component.html.Div {
ReproView(Prefs prefs) {
// Binding the session-scoped signal is what trips the cross-session check
getStyle().bind("background-color", prefs.color);
}
}
@SpringBootTest
class MultiUserReproTest {
@Autowired ApplicationContext applicationContext;
@Test
void twoUsersEachGetOwnSessionScopedBean() {
try (var app = SpringBrowserlessApplicationContext
.createSecured(applicationContext, ReproView.class);
var alice = app.newUser("alice", "USER");
var bob = app.newUser("bob", "USER")) {
alice.newWindow().navigate(ReproView.class); // OK
bob.newWindow().navigate(ReproView.class); // throws: ValueSignal
// created in another session
}
}
}
(Replace Prefs with any real @SessionScope / @VaadinSessionScope bean that
exposes a local ValueSignal or ListSignal.)
Likely cause
Each BrowserlessUserContext creates its own session objects, but Spring's
session-scope resolution (RequestContextHolder / the scoped bean store) does not
appear to be (re)bound to the active user's session when
BrowserlessUIContext.activate() installs that user's thread-locals. So the
first resolution of a session-scoped bean is cached and handed to every user.
Suggested direction
On user/window activation, bind the Spring request/session context
(RequestContextHolder, and the @VaadinSessionScope store) to the active
BrowserlessUserContext's session so that session-scoped beans resolve per user.
A regression test asserting two users receive distinct @SessionScope and
@VaadinSessionScope bean instances (and distinct local signals) would lock it
in.
Impact
High for collaborative apps: the whole point of the multi-user API is testing
cross-session collaboration, and those apps almost always keep per-session UI
state (current user, preferences, presence) in session-scoped signal beans —
exactly the beans that currently leak across users.
Summary
When using the 1.1 multi-user API (
SpringBrowserlessApplicationContext→newUser()→newWindow()), each user gets its ownVaadinSession, butSpring-managed session-scoped beans (
@SessionScopeand, by extension,@VaadinSessionScope) are not resolved per user. The second user reuses thefirst user's session-scoped bean instance. When such a bean holds a local
ValueSignal(orListSignal), the second user's UI then accesses a signal thatwas created in the first user's
VaadinSession, which fails with:This blocks multi-user testing for any app whose views (or shared parent layout)
depend on a Spring session-scoped bean — a very common pattern.
Environment
Expected behavior
Each
BrowserlessUserContextcreated vianewUser(...)should resolve Springsession-scoped beans (
@SessionScope,@VaadinSessionScope) against that user'sown session, so two users get independent instances — mirroring two real browser
sessions. A signal stored in a
@SessionScopebean should be per-user.Actual behavior
The second user reuses the first user's session-scoped bean instance. The bean's
local
ValueSignalbelongs to the first user'sVaadinSession, so binding it inthe second user's UI throws the cross-session
IllegalStateExceptionabove andnavigation fails.
Key diagnostic
A
@SessionScopebean with a localValueSignal, bound in a view, rendersfine for one user.
This pins the gap to per-user session-scope isolation, not to the API surface,
signals support, or navigation in general.
Minimal reproduction
(Replace
Prefswith any real@SessionScope/@VaadinSessionScopebean thatexposes a local
ValueSignalorListSignal.)Likely cause
Each
BrowserlessUserContextcreates its own session objects, but Spring'ssession-scope resolution (
RequestContextHolder/ the scoped bean store) does notappear to be (re)bound to the active user's session when
BrowserlessUIContext.activate()installs that user's thread-locals. So thefirst resolution of a session-scoped bean is cached and handed to every user.
Suggested direction
On user/window activation, bind the Spring request/session context
(
RequestContextHolder, and the@VaadinSessionScopestore) to the activeBrowserlessUserContext's session so that session-scoped beans resolve per user.A regression test asserting two users receive distinct
@SessionScopeand@VaadinSessionScopebean instances (and distinct local signals) would lock itin.
Impact
High for collaborative apps: the whole point of the multi-user API is testing
cross-session collaboration, and those apps almost always keep per-session UI
state (current user, preferences, presence) in session-scoped signal beans —
exactly the beans that currently leak across users.