Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 17 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,11 @@ For tests that need to drive multiple users — or multiple browser windows for
the same user — against a single application, Browserless Test exposes a
layered context API that mirrors the Vaadin hierarchy:

| Context | Maps to | Created via |
|------------------------------------|----------------------------------|--------------------------------------------------------------------------------------|
| `BrowserlessApplicationContext<C>` | shared `VaadinServletService` | `BrowserlessApplicationContext.create(routes)` (or a framework factory, see below) |
| `BrowserlessUserContext` | one `VaadinSession` (one user) | `app.newUser()` / `app.newUser(credentials)` / `app.newUser(username, roles...)` |
| `BrowserlessUIContext` | one `UI` (one browser window) | `user.newWindow()` |
| Context | Maps to | Created via |
|------------------------------------|----------------------------------|--------------------------------------------------------------------------------------------|
| `BrowserlessApplicationContext<C>` | shared `VaadinServletService` | `BrowserlessApplicationContext.create(viewPackagesOrClasses)` (or a framework factory) |
| `BrowserlessUserContext` | one `VaadinSession` (one user) | `app.newUser()` / `app.newUser(credentials)` / `app.newUser(username, roles...)` |
| `BrowserlessUIContext` | one `UI` (one browser window) | `user.newWindow()` |

`BrowserlessUIContext` exposes the same DSL as `BrowserlessTest` (`navigate`,
`find`, `findInView`, `test`, `roundTrip`). Every DSL call automatically activates the
Expand All @@ -216,7 +216,6 @@ fires destroy listeners, and clears Vaadin and security thread-locals.

```java
import com.vaadin.browserless.BrowserlessApplicationContext;
import com.vaadin.browserless.internal.Routes;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Paragraph;
import org.junit.jupiter.api.Test;
Expand All @@ -226,9 +225,8 @@ class SharedCounterTest {

@Test
void twoUsersShareApplicationState() {
Routes routes = new Routes()
.autoDiscoverViews(SharedCounterView.class.getPackageName());
try (var app = BrowserlessApplicationContext.create(routes)) {
try (var app = BrowserlessApplicationContext
.create(SharedCounterView.class)) {
var w1 = app.newUser().newWindow();
var w2 = app.newUser().newWindow();

Expand All @@ -250,18 +248,17 @@ class SharedCounterTest {

### Spring

`SpringBrowserlessApplicationContext.create(routes, springCtx)` wires the
application context to the Spring `ApplicationContext` and (when Spring
Security is on the classpath) installs a `SecurityContextHandler` so per-user
authentication is automatically isolated across windows. The
`SpringBrowserlessApplicationContext.create(springCtx, viewPackagesOrClasses)`
wires the application context to the Spring `ApplicationContext` and (when
Spring Security is on the classpath) installs a `SecurityContextHandler` so
per-user authentication is automatically isolated across windows. The
`newUser(username, roles...)` shorthand mirrors `@WithMockUser`.

```java
import com.testapp.security.LoginView;
import com.testapp.security.ProtectedView;
import com.vaadin.browserless.SecuredBrowserlessApplicationContext;
import com.vaadin.browserless.SpringBrowserlessApplicationContext;
import com.vaadin.browserless.internal.Routes;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -280,10 +277,9 @@ class MultiUserSecurityTest {

@Test
void securityContextIsIsolatedPerUser() {
Routes routes = new Routes().autoDiscoverViews("com.testapp.security");
try (SecuredBrowserlessApplicationContext<Authentication> app =
SpringBrowserlessApplicationContext.createSecured(routes,
springCtx)) {
SpringBrowserlessApplicationContext.createSecured(
springCtx, "com.testapp.security")) {

var adminWindow = app.newUser("john", "USER").newWindow();
var anonWindow = app.newUser().newWindow();
Expand All @@ -309,16 +305,15 @@ hand-built `Authentication` token.

### Quarkus

`QuarkusBrowserlessApplicationContext.create(routes)` resolves Quarkus beans
through CDI and installs a `SecurityContextHandler` backed by
`QuarkusBrowserlessApplicationContext.create(viewPackagesOrClasses)` resolves
Quarkus beans through CDI and installs a `SecurityContextHandler` backed by
`CurrentIdentityAssociation`. The `newUser(username, roles...)` shorthand
builds a matching `QuarkusSecurityIdentity`.

```java
import com.testapp.security.LoginView;
import com.testapp.security.ProtectedView;
import com.vaadin.browserless.SecuredBrowserlessApplicationContext;
import com.vaadin.browserless.internal.Routes;
import com.vaadin.browserless.quarkus.QuarkusBrowserlessApplicationContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.junit.QuarkusTest;
Expand All @@ -330,9 +325,9 @@ class MultiUserSecurityTest {

@Test
void securityContextIsIsolatedPerUser() {
Routes routes = new Routes().autoDiscoverViews("com.testapp.security");
try (SecuredBrowserlessApplicationContext<SecurityIdentity> app =
QuarkusBrowserlessApplicationContext.createSecured(routes)) {
QuarkusBrowserlessApplicationContext
.createSecured("com.testapp.security")) {

var adminWindow = app.newUser("john", "USER").newWindow();
var anonWindow = app.newUser().newWindow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ protected void addComponentTesterPackages(String... packages) {
componentTesterPackages.addAll(Arrays.asList(packages));
}

protected void addComponentTesterPackages(Class<?>... classes) {
Stream.of(classes).map(Class::getPackageName)
.forEach(componentTesterPackages::add);
}

// --- Lifecycle callbacks ---

protected void doInit(Object testInstance, ExtensionContext ctx) {
Expand Down Expand Up @@ -104,11 +109,8 @@ private void standaloneInit(Class<?> testClass) {
if (testerAnnotation != null) {
testerPkgs.addAll(Arrays.asList(testerAnnotation.value()));
}
for (String pkg : testerPkgs) {
if (BaseBrowserlessTest.scanned.add(pkg)) {
BaseBrowserlessTest.testers
.putAll(BaseBrowserlessTest.scanForTesters(pkg));
}
if (!testerPkgs.isEmpty()) {
TesterRegistry.registerPackages(testerPkgs.toArray(String[]::new));
}

// Resolve view packages from annotation and programmatic config
Expand All @@ -125,7 +127,7 @@ private void standaloneInit(Class<?> testClass) {
}
packages.removeIf(Objects::isNull);

Routes routes = BaseBrowserlessTest.discoverRoutes(packages);
Routes routes = RouteDiscovery.discover(packages);
MockVaadin.setup(routes, MockedUI::new, services);
signalsTestEnvironment = TestSignalEnvironment.register();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ public BrowserlessClassExtension withComponentTesterPackages(
return this;
}

/**
* Adds the packages of the given classes to the set of packages to scan for
* {@link ComponentTester} implementations.
*
* @param classes
* classes whose packages should be scanned for testers
* @return this extension instance
*/
public BrowserlessClassExtension withComponentTesterPackages(
Class<?>... classes) {
addComponentTesterPackages(classes);
return this;
}

@Override
public void beforeAll(ExtensionContext ctx) {
doInit(ctx.getTestInstance().orElse(null), ctx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ public BrowserlessExtension withComponentTesterPackages(
return this;
}

/**
* Adds the packages of the given classes to the set of packages to scan for
* {@link ComponentTester} implementations.
*
* @param classes
* classes whose packages should be scanned for testers
* @return this extension instance
*/
public BrowserlessExtension withComponentTesterPackages(
Class<?>... classes) {
addComponentTesterPackages(classes);
return this;
}

@Override
public void beforeEach(ExtensionContext ctx) {
doInit(ctx.getTestInstance().orElse(null), ctx);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2000-2026 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.browserless;

import com.example.base.signals.SignalsView;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import com.vaadin.browserless.builderfixtures.WidgetComponent;
import com.vaadin.browserless.builderfixtures.WidgetTester;

/**
* Exercises the new {@link BrowserlessApplicationContext} factory and builder
* surface introduced to close
* <a href="https://github.com/vaadin/browserless-test/issues/61">issue #61</a>:
* factory overloads that take view packages instead of an internal
* {@code Routes}, and a builder method that scans for custom
* {@link ComponentTester} implementations.
*/
class BrowserlessApplicationContextBuilderTest {

@Test
void builderWithComponentTesterPackages_resolvesCustomTester() {
try (var app = BrowserlessApplicationContext
.create(b -> b.withViewPackages(SignalsView.class)
.withComponentTesterPackages(WidgetComponent.class))) {
var window = app.newUser().newWindow();
ComponentTester<?> tester = window.test(new WidgetComponent());
Assertions.assertInstanceOf(WidgetTester.class, tester,
"Custom tester scanned via the builder should resolve");
}
}

@Test
void createWithViewPackageClass_opensContextWithoutRoutesArg() {
try (var app = BrowserlessApplicationContext
.create(SignalsView.class)) {
var window = app.newUser().newWindow();
var view = window.navigate(SignalsView.class);
Assertions.assertNotNull(view);
}
}

@Test
void createWithViewPackageString_opensContextWithoutRoutesArg() {
try (var app = BrowserlessApplicationContext
.create(SignalsView.class.getPackageName())) {
var window = app.newUser().newWindow();
var view = window.navigate(SignalsView.class);
Assertions.assertNotNull(view);
}
}

@Test
void createWithUnsecuredConfigurer_returnsUnsecuredContext() {
try (var app = BrowserlessApplicationContext
.create(b -> b.withViewPackages(SignalsView.class))) {
// Compile-time check on the variable type is enough; runtime
// assertion guards against a regression that hands back a
// subclass with security wiring.
Assertions.assertFalse(
app instanceof SecuredBrowserlessApplicationContext<?>,
"Unsecured configurer must not return a secured context");
var window = app.newUser().newWindow();
Assertions.assertNotNull(window.navigate(SignalsView.class));
}
}

@Test
void createSecuredWithConfigurer_returnsSecuredContext() {
SecurityContextHandler<String> handler = new RecordingHandler();

SecuredBrowserlessApplicationContext<String> app = BrowserlessApplicationContext
.createSecured(b -> b.withViewPackages(SignalsView.class)
.withSecurityContextHandler(handler));
try (app) {
// The variable type is the proof that the configurer returned
// a SecuredBuilder<C> and the factory returned the matching
// secured context type.
Assertions.assertSame(handler, app.getSecurityContextHandler(),
"Configurer-installed handler must propagate to the"
+ " built context");
var window = app.newUser("alice").newWindow();
Assertions.assertNotNull(window.navigate(SignalsView.class));
}
}

private static final class RecordingHandler
implements SecurityContextHandler<String> {
@Override
public void setupAuthentication(String credentials) {
}

@Override
public Object saveContext() {
return null;
}

@Override
public void restoreContext(Object snapshot) {
}

@Override
public void clearContext() {
}

@Override
public String createCredentials(String username, String... roles) {
return username;
}
}
}
Loading
Loading