Guide Claude on writing end-to-end browser tests with Vaadin TestBench in Vaadin 25. This skill should be used when the user asks to "write an end-to-end test", "write a browser test", "use TestBench", "create a page object", "test in a real browser", "integration test a Vaadin app", "visual regression test", "cross-browser test", or needs help with TestBench Element API, ElementQuery, page objects, or TestBenchTestCase.
64
77%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./skills/testbench-testing/SKILL.mdUse the Vaadin MCP tools (search_vaadin_docs) to look up the latest documentation whenever uncertain about a specific API detail. Always set vaadin_version to "25" and ui_language to "java".
Note: TestBench requires a commercial Vaadin subscription.
TestBench runs your Vaadin application in a real browser (Chrome, Firefox, etc.) and lets you write Java tests that interact with it like a user would. It's built on Selenium but provides a high-level API specifically designed for Vaadin components.
Use end-to-end TestBench tests for:
For everything else, prefer UI unit tests (see the ui-unit-testing skill) — they're faster and less flaky.
An Element class represents a DOM element — either a built-in HTML element (<div>, <span>) or a Vaadin web component (<vaadin-button>, <vaadin-grid>). Elements provide component-specific methods for interaction.
Every Vaadin component has a corresponding Element class: ButtonElement, TextFieldElement, GridElement, ComboBoxElement, etc.
ElementQuery finds elements on the page. Use the $() method:
// Find by type
ButtonElement button = $(ButtonElement.class).first();
// Find by ID
TextFieldElement name = $(TextFieldElement.class).id("name");
// Find all buttons
List<ButtonElement> buttons = $(ButtonElement.class).all();
// Wait for element to appear
ButtonElement btn = $(ButtonElement.class).waitForFirst();
// Find by attribute
$(DivElement.class).attribute("class", "active").first();
// Nested query — find inside another element
VerticalLayoutElement layout = $(VerticalLayoutElement.class).id("content");
ButtonElement innerBtn = layout.$(ButtonElement.class).first();Key methods on ElementQuery:
id("id") — find by id (returns single element)first() — first matchlast() — last matchget(n) — nth matchall() — all matches as listexists() — boolean checkwaitForFirst() — waits until a match appearsattribute("name", "value") — filter by attributepublic class LoginTest extends BrowserTestBase {
@BrowserTest
public void loginWithValidCredentials() {
$(TextFieldElement.class).id("username").setValue("admin");
$(PasswordFieldElement.class).id("password").setValue("secret");
$(ButtonElement.class).id("login").click();
// Verify navigation to dashboard
assertTrue($(DivElement.class).id("dashboard").exists());
}
}public class LoginTest extends TestBenchTestCase {
@Before
public void setup() throws Exception {
setDriver(new ChromeDriver());
getDriver().get("http://localhost:8080");
}
@Test
public void loginWithValidCredentials() {
$(TextFieldElement.class).id("username").setValue("admin");
$(PasswordFieldElement.class).id("password").setValue("secret");
$(ButtonElement.class).id("login").click();
assertTrue($(DivElement.class).id("dashboard").exists());
}
@After
public void teardown() {
getDriver().quit();
}
}Page objects encapsulate interaction with a specific view or component, keeping test methods clean and maintainable. If the UI changes, only the page object needs updating — not every test.
A page object extends TestBenchElement and uses @Element("tag-name"):
@Element("div")
@Attribute(name = "class", contains = "login-view")
public class LoginViewElement extends TestBenchElement {
public void login(String username, String password) {
$(TextFieldElement.class).id("username").setValue(username);
$(PasswordFieldElement.class).id("password").setValue(password);
$(ButtonElement.class).id("login").click();
}
public boolean isLoginFailed() {
return $(DivElement.class).attribute("class", "error").exists();
}
}@BrowserTest
public void loginSuccess() {
LoginViewElement loginView = $(LoginViewElement.class).waitForFirst();
loginView.login("admin", "secret");
assertTrue($(DashboardViewElement.class).exists());
}
@BrowserTest
public void loginFailure() {
LoginViewElement loginView = $(LoginViewElement.class).waitForFirst();
loginView.login("admin", "wrong");
assertTrue(loginView.isLoginFailed());
}Use @Attribute to match page objects to DOM elements:
// Match by class attribute (contains for multi-value attributes)
@Element("div")
@Attribute(name = "class", contains = "my-view")
// Auto-match by simple class name (removes Element/PageObject suffix)
@Element("div")
@Attribute(name = "class", contains = Attribute.SIMPLE_CLASS_NAME)GridElement grid = $(GridElement.class).first();
// Get row count
int rowCount = grid.getRowCount();
// Get cell content
String name = grid.getCell(0, 0).getText();
// Click a row
grid.getRow(0).click();
// Scroll to a row
grid.scrollToRow(50);ComboBoxElement combo = $(ComboBoxElement.class).id("country");
combo.openPopup();
combo.selectByText("Finland");// Dialogs overlay the main content
DialogElement dialog = $(DialogElement.class).waitForFirst();
dialog.$(ButtonElement.class).id("confirm").click();waitForFirst() instead of first() — when elements might not be immediately present (after navigation, async loading).component.setId("login-button") makes them easy to find in tests. Prefer IDs over positional queries.waitForFirst() or waitUntil() instead of Thread.sleep(). Explicit waits are more reliable and faster.e47fdfe
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.