Skip to content

docs: Document how the browserless test environment differs from a running application - #6076

Open
mcollovati wants to merge 2 commits into
mainfrom
docs/browserless-environment-differences
Open

mcollovati wants to merge 2 commits into
mainfrom
docs/browserless-environment-differences

Conversation

@mcollovati

Copy link
Copy Markdown
Contributor

Follow-up to vaadin/browserless-test#201, which reported four ways a browserless test behaves differently from a running application. Two of them were bugs and are fixed in the framework; two are consequences of the test lifecycle and were never documented. This covers all of it on the docs side.

New page: Environment Differences

articles/flow/testing/browserless/environment-differences.adoc, placed right after Getting Started.

  • Test lifecycle — the order in which a test method sets things up: Spring field injection, then the security context, then the Vaadin environment and the initial navigation, then the test body. Neither of the two items below is derivable without it.
  • Session-scoped beans — a @VaadinSessionScope bean can't be autowired into a test class field, because the field is injected before a session exists. Worth noting that it fails every test in the class, not only the ones using the bean. Resolve it from the test method with getBean(...), ObjectProvider, or @Lazy.
  • Authentication applied during a test — a sign-in performed after setup leaves the view rendered for the previous user in place, which reads as a broken security configuration. Navigate again. reload() is not a substitute, since it re-renders the location access control already redirected to.

Getting Started

The Navigating to Views section listed only the path and parameter forms. A location string can carry a query string and a fragment as of vaadin/browserless-test#204, and that was never documented.

Spring Security

The page states that authentication details are available before the UI is created and the default route is navigated to. Added the boundary: that holds when the authentication is in place before the test method starts.


Draft for review of scope and placement. The behavior described was verified against the current browserless-test main.

…application

Adds an Environment Differences page covering the browserless test
lifecycle, why session-scoped beans cannot be injected into a test
class field, and why authentication established during a test needs a
second navigation.

Also documents that a location string passed to `navigate()` can carry
a query string and a fragment, and adds a note to the Spring Security
page about signing in after the environment is created.

Related to vaadin/browserless-test#201
@mcollovati mcollovati changed the title Document how the browserless test environment differs from a running application docs: Document how the browserless test environment differs from a running application Sep 18, 2026
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

This PR has been deployed for preview.

URL: https://docs-preview-pr-6076.fly.dev

Changed pages

Added content is highlighted in green; removed content is marked in red on each page.

Built from 4593504

@mcollovati
mcollovati marked this pull request as ready for review September 18, 2026 09:28
@peholmst peholmst added the target/v25.3 Automatically cherry-pick to the v25.3 branch label Sep 18, 2026
}
----

Injecting [classname]`ObjectProvider<Cart>` or annotating the field with [annotationname]`@Lazy` works as well, because both defer resolution until the bean is first used.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be an easier to use method to suggest as the default, instead of injecting ApplicationContext?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agreed — it is now the default in the example, with @Lazy and ApplicationContext.getBean(...) mentioned after it as equivalents. @VaadinSessionScope has no proxyMode, so the only thing that matters is that the lookup happens inside the test method rather than at field injection time, and ObjectProvider says that with the least ceremony.


.Reloading Isn't Enough
[NOTE]
[methodname]`reload()` isn't a substitute for navigating again. It re-renders the current location, and when access control has redirected to the login view, that location is the login view.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this how it should work or a limitation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How it should work. MockPage.reload() closes the UI, recording internals.activeViewLocation, and MockVaadin.createUI navigates the new UI back to that location — which is exactly what pressing F5 does in a browser. Since access control redirected the earlier navigation to the login view, that recorded location is the login view. The note now says it mirrors a browser reload, instead of reading like a shortcoming.

@WithMockUser(username = "admin", roles = "ADMIN",
setupBefore = TestExecutionEvent.TEST_EXECUTION)
void adminSignsIn_adminViewShown() {
// The initial navigation ran while the user was still anonymous.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By just looking at the code example, it's not clear what this refers to as there is no "initial navigation" anywhere

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded to say what happened instead of naming it: "Setup navigated to the root route while the user was still anonymous, and access control redirected that navigation to the login view."



[#authentication-applied-during-a-test]
== Authentication Applied During a Test

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you only have a quick look at this page, you might very well think this is how you should to authentication in a test. Could it maybe start by showing the correct way and only after that explain why you would want another approach and how you deal with things if you pick that approach?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restructured that way: the section now opens with a plain @WithMockUser and a test that just navigates, and only then covers a sign-in performed during the test, why the view does not follow it, and the re-navigation. The "prefer doing it before the test method" paragraph that used to close the section is gone, since that is now the opening.


All navigation methods return the instantiated view, so that its fields can be used directly for testing.

A location string can also [since:com.vaadin:vaadin@V25.3]#carry a query string, a fragment, or both#. They are split off from the path the same way the browser address bar does it, so the query parameters reach the view through the navigation event:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be another bullet point in the list above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — it is a fourth bullet now, in the same shape as the other three, with the split-up and fragment-only details as its continuation.

navigate("orders/1?tab=history&page=2", OrderView.class);
----

Query parameters can be passed as a separate [classname]`QueryParameters` object through [methodname]`UI.getCurrent().navigate(String, QueryParameters)`. Providing them both ways at once is rejected, because the query string in the location would be dropped.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the wrong way to do it as then it would simulate a server side navigation instead? Could just drop this if you can use navigate with parameters

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, and it is dropped. There is no navigate(String, QueryParameters) on the test base class, so that line went through UI directly, which skips the target view validation the navigate() methods do. The location string carries the query parameters since vaadin/browserless-test#204, so nothing is lost by removing it.

page-title: How the Vaadin browserless test environment differs
description: What the browserless environment creates, in which order, and which application behavior therefore needs a different approach in a test.
meta-description: Learn the browserless test lifecycle in Vaadin, and how it affects session-scoped beans and authentication applied during a test.
order: 12

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page seems way to high up, it is more of an advanced topic that should be closed to the end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to order: 80, so it sits after the framework-specific pages and before Optimizing Tests and Migration.

Move the page to the end of the browserless section, since it is an
advanced topic rather than something to read right after Getting Started.

Lead the authentication section with the approach that works without a
second navigation, and only then describe a sign-in performed during the
test. Make the example's comment say what the setup navigation did, since
the code around it never mentions one. Say that Page.reload() recreating
the UI at the active location is what a browser reload does, not a
limitation.

Suggest ObjectProvider as the default way to reach a session scoped bean,
and keep @lazy and ApplicationContext.getBean as alternatives.

In Getting Started, fold navigation with a query string into the list of
navigate() forms, and drop the UI.getCurrent().navigate(String,
QueryParameters) variant: it bypasses the target view validation the
navigate() methods perform, and the location string already carries the
query parameters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flow target/v25.3 Automatically cherry-pick to the v25.3 branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants