diff --git a/articles/flow/configuration/feature-flags.adoc b/articles/flow/configuration/feature-flags.adoc index 9bd5f51c85..adaac8c444 100644 --- a/articles/flow/configuration/feature-flags.adoc +++ b/articles/flow/configuration/feature-flags.adoc @@ -78,6 +78,8 @@ Enables <<{articles}/hilla/guides/full-stack-signals#, Hilla Full-stack Signals> Feature flags can be managed in different ways: by using Vaadin Copilot; by editing the feature flags properties file; or by setting Java system properties. Each of these methods is described in the sub-sections that follow. As you read, keep in mind that system properties always have the highest priority. Since they're not written into the feature flags properties file, they're valid only for a single execution. +A <<{articles}/flow/testing/browserless/test-configuration#, browserless test>> is the exception: it can enable a flag for a single test class or test method, overriding all of the sources below. + === Vaadin Copilot diff --git a/articles/flow/testing/browserless/extensions.adoc b/articles/flow/testing/browserless/extensions.adoc index 797daf6dcd..93a64d0e2a 100644 --- a/articles/flow/testing/browserless/extensions.adoc +++ b/articles/flow/testing/browserless/extensions.adoc @@ -102,8 +102,19 @@ Both extensions support a builder-style API for configuration, used as an altern | [methodname]`withComponentTesterPackages(String...)` | Adds packages to scan for custom [classname]`ComponentTester` implementations. Equivalent to [annotationname]`@ComponentTesterPackages`. + +| [since:com.vaadin:vaadin@V25.3]#[methodname]`withApplicationProperty(String, String)`# +| Sets a Vaadin application property for the environment the extension creates. [methodname]`withApplicationProperties(Map)` sets several at once. + +| [since:com.vaadin:vaadin@V25.3]#[methodname]`withFeatureFlags(String...)`# +| Enables the given feature flags. [methodname]`withFeatureFlag(String, boolean)` enables or disables a single flag. Both methods also have a [classname]`Feature` overload. + +| [since:com.vaadin:vaadin@V25.3]#[methodname]`withConfiguration(BrowserlessConfiguration)`# +| Applies a configuration built elsewhere as the baseline that the other methods add to, so that several test classes can share it. |=== +Application properties, feature flags, and [classname]`Lookup` services can also be declared with the [annotationname]`@BrowserlessTestConfig` annotation. On [classname]`BrowserlessExtension` it works both on the test class and on a single test method; [classname]`BrowserlessClassExtension` creates one environment for the whole class, so there the annotation belongs on the test class. See <>. + The [annotationname]`@ViewPackages` annotation still works when placed on the test class; programmatic configuration adds to what the annotation declares. .Extension with Programmatic Configuration diff --git a/articles/flow/testing/browserless/multi-user.adoc b/articles/flow/testing/browserless/multi-user.adoc index 5d3b3ad2ad..d4ff290c6d 100644 --- a/articles/flow/testing/browserless/multi-user.adoc +++ b/articles/flow/testing/browserless/multi-user.adoc @@ -346,8 +346,19 @@ For advanced setups, [classname]`BrowserlessApplicationContext.Builder` exposes | [methodname]`withCloseHook(Runnable)` | Registers a callback to run after the context tears down -- intended for releasing framework-specific state. + +| [since:com.vaadin:vaadin@V25.3]#[methodname]`withApplicationProperty(String, String)` / [methodname]`withApplicationProperties(Map)`# +| Sets Vaadin application properties for the environment the context creates. + +| [since:com.vaadin:vaadin@V25.3]#[methodname]`withFeatureFlags(String...)` / [methodname]`withFeatureFlag(String, boolean)`# +| Enables or disables feature flags for the environment the context creates. Both methods also have a [classname]`Feature` overload. + +| [since:com.vaadin:vaadin@V25.3]#[methodname]`withConfiguration(BrowserlessConfiguration)`# +| Applies a configuration built elsewhere as the baseline that the other methods add to, such as [methodname]`BrowserlessConfiguration.from(getClass())` to reuse what the test class declares with [annotationname]`@BrowserlessTestConfig`. |=== +The property, feature flag, and configuration methods are the programmatic form of [annotationname]`@BrowserlessTestConfig`, which the context itself does not read; see <>. [classname]`SecuredBrowserlessApplicationContext.Builder` exposes the same methods. + For one-off tweaks without holding on to a builder reference, [methodname]`BrowserlessApplicationContext.create(UnaryOperator)` and the corresponding [methodname]`createSecured(Function>)` accept a configurer: [source,java] diff --git a/articles/flow/testing/browserless/test-configuration.adoc b/articles/flow/testing/browserless/test-configuration.adoc new file mode 100644 index 0000000000..b90b9f91d7 --- /dev/null +++ b/articles/flow/testing/browserless/test-configuration.adoc @@ -0,0 +1,134 @@ +--- +title: Test Configuration +page-title: How to configure the Vaadin environment of a browserless test +description: Set Vaadin application properties, feature flags, and Lookup services for a single test class or test method. +meta-description: Configure Vaadin application properties, feature flags, and Lookup services for a single browserless test with the BrowserlessTestConfig annotation. +order: 46 +--- + + += [since:com.vaadin:vaadin@V25.3]#Test Configuration# + +Some tests need a Vaadin environment configured differently from the rest of the suite: a view behind a feature flag, or a setting such as `devmode.sessionSerialization.enabled`. Annotate the test class, or a single test method, with [annotationname]`@BrowserlessTestConfig`: + +[source,java] +---- +@ViewPackages(classes = CartView.class) +@BrowserlessTestConfig( + applicationProperties = "devmode.sessionSerialization.enabled=true", + featureFlags = "myExperimentalFeature") +class CartViewTest extends BrowserlessTest { + + @Test + void experimentalCheckoutIsShown() { + // The feature flag is enabled for this test + } + + @Test + @BrowserlessTestConfig(featureFlags = "myExperimentalFeature=false") + void fallbackCheckoutIsShown() { + // The same class, with the flag off for this method only + } +} +---- + +Each setting applies to the Vaadin environment created for the annotated test, so there is nothing to reset afterwards, and nothing leaks into the next test. + + +== Settings + +[cols="1,2"] +|=== +| Attribute | Description + +| [propertyname]`applicationProperties` +| `name=value` pairs applied to the Vaadin deployment configuration, such as `"devmode.sessionSerialization.enabled=true"`. The value is everything after the first `=`, so a value can itself contain `=`. The properties are set before the servlet starts, so code that runs at startup, such as a [interfacename]`VaadinServiceInitListener`, already sees them. + +| [propertyname]`featureFlags` +| Either a feature identifier, to enable the feature, or an `id=true\|false` pair. These flags override the <<{articles}/flow/configuration/feature-flags#, feature flag>> sources that apply outside a test -- the [filename]`vaadin-featureflags.properties` file and the `vaadin.experimental.*` system properties. Toggling a flag this way needs no development mode and writes nothing into the project folder. An unknown identifier fails with an error that lists the available flags. + +| [propertyname]`lookupServices` +| Implementation classes registered with the Vaadin [classname]`Lookup`, such as an [interfacename]`InstantiatorFactory` or a [interfacename]`ResourceProvider`. +|=== + +The `browserless` application property itself stays enforced and cannot be overridden. + + +== Merging Class and Method Configuration + +Every annotation a test inherits contributes to the configuration, rather than being shadowed by the nearest one. The merge works entry by entry: a property name or feature identifier declared in more than one place takes the value of the highest-ranking declaration, while the names declared only once all apply. The closer a declaration is to the test method, the higher it ranks: the method first, then the test class, then superclasses from the nearest up, and then, for a [annotationname]`@Nested` test, enclosing classes from the innermost out. In the example at the top of this page, the method-level `myExperimentalFeature=false` therefore replaces the value the test class declares for that flag, and leaves `devmode.sessionSerialization.enabled` untouched. + +[source,java] +---- +@BrowserlessTestConfig(applicationProperties = "base.property=fromBase") +abstract class AbstractViewTest extends BrowserlessTest { +} + +@BrowserlessTestConfig(featureFlags = "myExperimentalFeature") +class CartViewTest extends AbstractViewTest { + // Both base.property and myExperimentalFeature apply +} +---- + +Lookup services are the exception to the ranking: they have no name to resolve, so every declared service is registered. A test method can add a service, but cannot remove one that its test class declares. The services that the Spring and Quarkus integrations need are always registered, and the test configuration never affects them. + +A method-level annotation needs an environment built for each test method. When one environment is shared by the whole class, as with [classname]`BrowserlessClassExtension`, the annotation is rejected with an error naming the methods that carry it. Move it to the test class in that case. + + +== Configuring Without Annotations + +The same settings can be built in code. On a JUnit 6 extension: + +[source,java] +---- +@RegisterExtension +BrowserlessExtension extension = new BrowserlessExtension() + .withApplicationProperty("devmode.sessionSerialization.enabled", "true") + .withFeatureFlags("myExperimentalFeature"); +---- + +On the application context builder of a multi-user test: + +[source,java] +---- +try (var app = BrowserlessApplicationContext.create(builder -> builder + .withViewPackages(CartView.class) + .withFeatureFlags("myExperimentalFeature"))) { + // ... +} +---- + +A [classname]`BrowserlessApplicationContext` is created in plain code rather than by a JUnit extension, so it never looks at [annotationname]`@BrowserlessTestConfig`: only what its own builder declares applies. To reuse the configuration declared by a test class, pass it explicitly with [methodname]`BrowserlessConfiguration.from(...)`: + +[source,java] +---- +try (var app = BrowserlessApplicationContext.create(builder -> builder + .withViewPackages(CartView.class) + .withConfiguration(BrowserlessConfiguration.from(getClass())))) { + // ... +} +---- + +[methodname]`from(...)` reads a single annotation -- the one declared on the given class, or the one inherited from its nearest annotated superclass -- rather than merging the whole hierarchy the way an extension does. + +Or by overriding [methodname]`testConfiguration()` on a test that extends a base class: + +[source,java] +---- +@Override +protected BrowserlessConfiguration testConfiguration() { + return BrowserlessConfiguration.builder() + .withConfiguration(super.testConfiguration()) + .withFeatureFlags("myExperimentalFeature") + .build(); +} +---- + +On an extension, a configuration built in code wins over the class-level annotation, and loses against the method-level one. A [methodname]`testConfiguration()` override ranks differently: [methodname]`super.testConfiguration()` returns the configuration already resolved from the annotations, so whatever the override adds on top of it wins over all of them, the method-level annotation included. Build on [methodname]`super.testConfiguration()` to refine the declared configuration, and leave out the values that a test method needs to override. + +.Spring Properties Win +[NOTE] +With Spring, a Vaadin property defined in the Spring environment, such as `vaadin.devmode.sessionSerialization.enabled` in [filename]`application.properties`, is applied by [classname]`SpringServlet` on top of the test configuration, and therefore wins over [annotationname]`@BrowserlessTestConfig`. Use [annotationname]`@TestPropertySource` to override such a property for a test. Properties that are not Vaadin init parameters are unaffected. + + +[discussion-id]`CD70CAA5-6506-4810-BED5-0F54DCE2F0FA` diff --git a/articles/upgrading/index.adoc b/articles/upgrading/index.adoc index 48dcb30ba4..7b54328101 100644 --- a/articles/upgrading/index.adoc +++ b/articles/upgrading/index.adoc @@ -1208,6 +1208,10 @@ A test written against the old behavior, and one that reaches for a cell the gri A [classname]`GridContextMenu` is always about a row, so its tester is obtained with [methodname]`test(grid).contextMenu(row)` and opened with [methodname]`open()`, or opened on a row directly with [methodname]`open(row)`. +=== Browserless Testing: Lookup Services of the Spring and Quarkus Integrations + +Starting with Vaadin 25.3, the services that the Spring and Quarkus integrations need have moved from [methodname]`lookupServices()` to the new [methodname]`frameworkLookupServices()` method -- both declared by [classname]`BaseBrowserlessTest`, the base class of [classname]`BrowserlessTest`, [classname]`SpringBrowserlessTest`, and [classname]`QuarkusBrowserlessTest` -- and are registered in every case. An override of [methodname]`lookupServices()` therefore adds to them instead of replacing them, which is what keeps those integrations working, and the resulting [classname]`Lookup` holds more services than before. Override [methodname]`frameworkLookupServices()` instead to replace one of the framework services, such as the Spring [classname]`SpringSecurityRequestCustomizer`. [methodname]`lookupServices()` itself is deprecated in favor of the test configuration, which declares the same services on a test class or a test method; existing overrides are still honored. See <<{articles}/flow/testing/browserless/test-configuration#,Test Configuration>>. + == Binder [methodname]`Binder.validate()` implementation has been changed to behave as its Javadoc states. In other words, [methodname]`Binder.validate()` no longer fails when bean level validators have been configured but no bean is currently set (i.e. [classname]`Binder` is used in buffered mode).