Skip to content
Draft
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
33 changes: 18 additions & 15 deletions articles/flow/testing/browserless/component-query.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,46 @@ To overcome this limitation, [classname]`BrowserlessTest` provides a component q

== Component Queries

You can get a [classname]`ComponentQuery` object by calling the [methodname]`$()` method, specifying the type of the component you are searching for.
You can get a [classname]`ComponentQuery` object by calling the [methodname]`find()` method, specifying the type of the component you are searching for.

Once the query is ready with all conditions configured, use a terminal operator to retrieve the components that it found. Examples of terminal operators are [methodname]`single()`, [methodname]`last()`, [methodname]`atIndex()`, [methodname]`all()`, and [methodname]`id()`.

[source,java]
----
// Get the TextField
TextField nameField = $(TextField.class).single();
TextField nameField = find(TextField.class).single();
----

[NOTE]
[classname]`BrowserlessTest` retains [methodname]`$()` and [methodname]`$view()` as deprecated aliases for [methodname]`find()` and [methodname]`findInView()`, scheduled for removal in 2.0. The newer composition-based APIs ([classname]`BrowserlessExtension`, [classname]`BrowserlessClassExtension`, and [classname]`BrowserlessUIContext`) only expose [methodname]`find()` and [methodname]`findInView()`.


=== Scoping Queries

You can also restrict search scope to the children of the current view by using the [methodname]`$view()` method, or even to another component by using [methodname]`$(MyComponent.class, rootComponent)`.
You can also restrict search scope to the children of the current view by using the [methodname]`findInView()` method, or even to another component by using [methodname]`find(MyComponent.class, rootComponent)`.

[source,java]
----
// Get the TextField in the current view
TextField nameField = $view(TextField.class).single();
TextField nameField = findInView(TextField.class).single();

// Get the TextField nested in a container
TextField nameField = $(TextField.class, view.formLayout).single();
TextField nameField = find(TextField.class, view.formLayout).single();
----

The query object has many filtering methods that can be used to refine the search.

[source,java]
----
// Get the TextField with the given label
TextField nameField = $view(TextField.class)
TextField nameField = findInView(TextField.class)
.withPropertyValue(TextField::getLabel, "First name")
.single();

// Get all TextFields in the view that satisfy the conditions
Predicate<TextField> fieldHasNotValue = field -> field.getOptionalValue().isEmpty();
Predicate<TextField> fieldIsInvalid = TextField::isInvalid;
List<TextField> textField = $view(TextField.class)
List<TextField> textField = findInView(TextField.class)
.withCondition(fieldHasNotValue.or(fieldIsInvalid))
.all();
----
Expand Down Expand Up @@ -127,13 +130,13 @@ Here are a few examples:
[source,java]
----
// Find a button by its text
Button save = $(Button.class).withText("Save").single();
Button save = find(Button.class).withText("Save").single();

// Find a TextField by CSS class
TextField styled = $(TextField.class).withClassName("highlighted").single();
TextField styled = find(TextField.class).withClassName("highlighted").single();

// Find checkboxes with a specific value
Checkbox checked = $(Checkbox.class).withValue(true).single();
Checkbox checked = find(Checkbox.class).withValue(true).single();
----


Expand All @@ -143,7 +146,7 @@ Use [methodname]`exists()` to check whether a query has results without throwing

[source,java]
----
if ($(Notification.class).exists()) {
if (find(Notification.class).exists()) {
// A notification is open
}
----
Expand All @@ -156,13 +159,13 @@ You can assert the number of results directly in the query chain:
[source,java]
----
// Expect exactly 3 text fields
List<TextField> fields = $(TextField.class).withResultsSize(3).all();
List<TextField> fields = find(TextField.class).withResultsSize(3).all();

// Expect between 1 and 5 results
List<Button> buttons = $(Button.class).withResultsSize(1, 5).all();
List<Button> buttons = find(Button.class).withResultsSize(1, 5).all();

// Expect at least 1 result
List<Grid> grids = $(Grid.class).withMinResults(1).all();
List<Grid> grids = find(Grid.class).withMinResults(1).all();
----


Expand All @@ -175,7 +178,7 @@ You may sometimes need to do a query for components nested inside the UI, in a h
----

// Search for all 'VerticalLayout's in the view
TextField textField = $view(VerticalLayout.class)
TextField textField = findInView(VerticalLayout.class)
// take the second one and start searching for 'TextField's
.thenOn(2, TextField.class)
// filter for disabled 'TextField's
Expand Down
130 changes: 130 additions & 0 deletions articles/flow/testing/browserless/extensions.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
title: JUnit 6 Extensions
page-title: Composition-based browserless testing with JUnit 6 extensions
description: Use BrowserlessExtension and BrowserlessClassExtension as an alternative to extending BrowserlessTest.
meta-description: Set up Vaadin browserless tests with JUnit 6 @RegisterExtension when extending a base class is not an option.
order: 45
---


= JUnit 6 Extensions

[since:com.vaadin:vaadin@V25.2]

Extending [classname]`BrowserlessTest` is the most compact way to write browserless tests, but it requires your test class to use inheritance for the Vaadin setup. When a project already has its own test base class, or you prefer composition over inheritance, the [classname]`BrowserlessExtension` and [classname]`BrowserlessClassExtension` JUnit 6 extensions provide the same functionality without requiring a specific superclass.

[NOTE]
These extensions are part of the `browserless-test-junit6` artifact. They don't replace [classname]`SpringBrowserlessTest` or [classname]`QuarkusBrowserlessTest` -- for Spring and Quarkus projects, continue to extend those base classes.


== When to Use an Extension

Use an extension when any of the following applies:

- The test class already extends another base class that can't be changed.
- The project standardizes on composition-based JUnit 6 extensions.


== Per-Method Lifecycle

[classname]`BrowserlessExtension` creates a fresh Vaadin environment before each test method and tears it down after. Register it as an instance field with [annotationname]`@RegisterExtension`:

[source,java]
----
@ViewPackages(classes = CartView.class)
class CartViewTest {

@RegisterExtension
BrowserlessExtension ext = new BrowserlessExtension();

@Test
void addItemToCart() {
CartView view = ext.navigate(CartView.class);
ext.test(view.getAddButton()).click();

Assertions.assertEquals(1, view.getCartSize());
}
}
----

Navigation, queries, and tester interactions are available as methods on the extension instance -- [methodname]`ext.navigate()`, [methodname]`ext.find()`, [methodname]`ext.findInView()`, [methodname]`ext.test()`, [methodname]`ext.getCurrentView()`, [methodname]`ext.fireShortcut()`, [methodname]`ext.roundTrip()`, and [methodname]`ext.runPendingSignalsTasks()`.


== Per-Class Lifecycle

[classname]`BrowserlessClassExtension` initializes the Vaadin environment once in [annotationname]`@BeforeAll` and shares it across all tests in the class. Register it as a `static` field:

[source,java]
----
@ViewPackages(classes = CartView.class)
class CartViewSharedTest {

@RegisterExtension
static BrowserlessClassExtension ext = new BrowserlessClassExtension();

@BeforeAll
static void setup() {
ext.navigate(CartView.class);
}

@Test
void addItem() {
// same UI instance as removeItem
ext.test(ext.find(Button.class).withText("Add").single()).click();
}

@Test
void removeItem() {
// state from addItem is preserved
}
}
----

The same trade-offs as <<optimizing-tests#sharing-the-vaadin-environment-across-tests, per-class lifecycle with `BrowserlessTest`>> apply: state leaks between tests, so write tests that tolerate leftover state or reset it explicitly.


== Configuring the Extension

Both extensions support a builder-style API for configuration, used as an alternative or in addition to annotations.

.Builder API
[cols="1,2"]
|===
| Method | Description

| [methodname]`withViewPackages(Class<?>...)`
| Adds the packages of the given classes to the route scan. Equivalent to [annotationname]`@ViewPackages(classes = ...)`.

| [methodname]`withViewPackages(String...)`
| Adds package names (as strings) to the route scan. Equivalent to [annotationname]`@ViewPackages(packages = ...)`.

| [methodname]`withServices(Class<?>...)`
| Registers custom implementations with the Vaadin [classname]`Lookup` SPI.

| [methodname]`withComponentTesterPackages(String...)`
| Adds packages to scan for custom [classname]`ComponentTester` implementations. Equivalent to [annotationname]`@ComponentTesterPackages`.
|===

The [annotationname]`@ViewPackages` annotation still works when placed on the test class; programmatic configuration adds to what the annotation declares.

.Extension with Programmatic Configuration
[source,java]
----
class AdminViewTest {

@RegisterExtension
BrowserlessExtension ext = new BrowserlessExtension()
.withViewPackages("com.example.views", "com.example.admin")
.withServices(CustomInstantiatorFactory.class)
.withComponentTesterPackages("com.example.testers");

@Test
void adminDashboardLoads() {
AdminDashboardView view = ext.navigate(AdminDashboardView.class);
Assertions.assertNotNull(view);
}
}
----


[discussion-id]`B51F9D4A-2E73-4B18-8C6F-9A3D7E2B1C04`
11 changes: 8 additions & 3 deletions articles/flow/testing/browserless/getting-started.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class HelloWorldViewTest extends SpringBrowserlessTest {
test(helloView.name).setValue("Test");
test(helloView.sayHello).click();

Notification notification = $(Notification.class).single();
Notification notification = find(Notification.class).single();
Assertions.assertEquals("Hello Test", test(notification).getText());
}

Expand Down Expand Up @@ -127,12 +127,12 @@ Each Vaadin component has a tester tailored to its behavior. For example, a [cla

=== Finding Components

Not every component is stored in a view field. For example, the [classname]`Notification` in the test above is created inside a click listener and isn't referenced anywhere in the view. Use the [methodname]`$()` query method to find components in the UI by their type:
Not every component is stored in a view field. For example, the [classname]`Notification` in the test above is created inside a click listener and isn't referenced anywhere in the view. Use the [methodname]`find()` query method to find components in the UI by their type:

[source,java]
----
// Find the single Notification currently open
Notification notification = $(Notification.class).single();
Notification notification = find(Notification.class).single();
----

The query API supports filtering by properties, predicates, and scoping to specific parts of the component tree. See <<component-query#, Querying Components>> for details.
Expand Down Expand Up @@ -215,4 +215,9 @@ fireShortcut(Key.KEY_S, KeyModifier.CONTROL);
----


.Multiple Users or Windows?
[TIP]
This setup drives a single user with a single window. For tests that need several concurrent users or multiple windows of the same user, see <<multi-user#, Multi-User and Multi-Window Testing>>.


[discussion-id]`7F423DA0-1C41-44BA-B832-55C269FA9311`
Loading
Loading