Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7aff9fee4f | |||
| a996269aff |
@@ -9,9 +9,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
./mvnw compile # compile only
|
./mvnw compile # compile only
|
||||||
./mvnw package # production build → target/*.jar
|
./mvnw package # production build → target/*.jar
|
||||||
java -jar target/*.jar # run production jar
|
java -jar target/*.jar # run production jar
|
||||||
|
./mvnw test # run all tests
|
||||||
```
|
```
|
||||||
|
|
||||||
No test sources exist yet (`src/test` is empty) — `spring-boot-starter-test` and `browserless-test-spring` are on the classpath but unused.
|
**Tests** live in two flavours, both run by surefire (`*Test`):
|
||||||
|
- `views/*Test` — browserless (`browserless-test-spring`): the Vaadin UI is built server-side and asserted on the component tree, no browser. Fast (seconds).
|
||||||
|
- `e2e/*PlaywrightTest` — real headless Chromium (`com.microsoft.playwright:playwright`) against the app on a random port, via `e2e/PlaywrightTestBase`. Slow (~1 min). Playwright downloads its browsers into `~/.cache/ms-playwright` on first run; if that fails (offline runner, missing system libs) the tests are **skipped** via a JUnit assumption instead of failing the build.
|
||||||
|
|
||||||
|
E2E assertions match the **English** captions: German is the unsuffixed fallback bundle (`translations.properties`), so `de` is not a provided locale and Vaadin serves `translations_en.properties` for a German browser. `PlaywrightTestBase` pins the context locale to `en-US` to make that explicit.
|
||||||
|
|
||||||
**Java 25 toolchain required** (`pom.xml` sets `java.version=25`). If the default JDK on PATH is older (check `java -version`), point `JAVA_HOME` at a Java 25 install for the Maven build, e.g.:
|
**Java 25 toolchain required** (`pom.xml` sets `java.version=25`). If the default JDK on PATH is older (check `java -version`), point `JAVA_HOME` at a Java 25 install for the Maven build, e.g.:
|
||||||
```bash
|
```bash
|
||||||
@@ -43,7 +48,7 @@ UI copy/data (chart labels, notifications) is in German.
|
|||||||
## AI-Autofix – Regeln
|
## AI-Autofix – Regeln
|
||||||
- Nur Issues mit Label `ai-ready` bearbeiten.
|
- Nur Issues mit Label `ai-ready` bearbeiten.
|
||||||
- Branch-Namensschema: `ai/issue-<nr>-<kurz-slug>`.
|
- Branch-Namensschema: `ai/issue-<nr>-<kurz-slug>`.
|
||||||
- Kein Fix ohne grüne Tests. Test-Befehl: `<dein Testkommando>`.
|
- Kein Fix ohne grüne Tests. Test-Befehl: `./mvnw test`.
|
||||||
- Bei Unklarheit / Scope > ~200 Zeilen Diff: NICHT fixen,
|
- Bei Unklarheit / Scope > ~200 Zeilen Diff: NICHT fixen,
|
||||||
stattdessen Issue kommentieren ("needs human") und Label `ai-blocked` setzen.
|
stattdessen Issue kommentieren ("needs human") und Label `ai-blocked` setzen.
|
||||||
- PR-Beschreibung MUSS `Closes #<nr>` enthalten.
|
- PR-Beschreibung MUSS `Closes #<nr>` enthalten.
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.example.e2e;
|
||||||
|
|
||||||
|
import com.microsoft.playwright.Locator;
|
||||||
|
import com.microsoft.playwright.Page;
|
||||||
|
import com.microsoft.playwright.options.AriaRole;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End-to-end example covering {@link com.example.views.FormView}: rendering,
|
||||||
|
* binder validation and the two form buttons. Captions are English — see
|
||||||
|
* {@link PlaywrightTestBase#openPage()} for why.
|
||||||
|
*/
|
||||||
|
class FormViewPlaywrightTest extends PlaywrightTestBase {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void openFormView() {
|
||||||
|
navigate("formular");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void form_rendersItsFieldsAndTheReadOnlyAge() {
|
||||||
|
assertThat(page.getByText("Registration")).isVisible();
|
||||||
|
assertThat(field("First name")).isVisible();
|
||||||
|
assertThat(field("Last name")).isVisible();
|
||||||
|
assertThat(field("Email")).isVisible();
|
||||||
|
// preset on the bean and bound read-only
|
||||||
|
assertThat(field("Age")).hasValue("37");
|
||||||
|
assertThat(field("Age")).not().isEditable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void save_withoutRequiredFields_showsErrorNotification() {
|
||||||
|
button("Save").click();
|
||||||
|
|
||||||
|
assertThat(notification()).containsText("Please check your entries");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void save_withInvalidEmail_showsErrorNotification() {
|
||||||
|
field("First name").fill("Max");
|
||||||
|
field("Last name").fill("Mustermann");
|
||||||
|
field("Email").fill("not-an-email");
|
||||||
|
|
||||||
|
button("Save").click();
|
||||||
|
|
||||||
|
assertThat(notification()).containsText("Please check your entries");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void save_withValidInput_showsSuccessNotification() {
|
||||||
|
field("First name").fill("Max");
|
||||||
|
field("Last name").fill("Mustermann");
|
||||||
|
field("Email").fill("max@example.com");
|
||||||
|
|
||||||
|
button("Save").click();
|
||||||
|
|
||||||
|
assertThat(notification()).containsText("Saved: Max Mustermann");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reset_clearsTheEnteredValues() {
|
||||||
|
field("First name").fill("Max");
|
||||||
|
field("Last name").fill("Mustermann");
|
||||||
|
|
||||||
|
button("Reset").click();
|
||||||
|
|
||||||
|
assertThat(field("First name")).hasValue("");
|
||||||
|
assertThat(field("Last name")).hasValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The input inside the Vaadin field carrying {@code label}. */
|
||||||
|
private Locator field(String label) {
|
||||||
|
return page.getByLabel(label, new Page.GetByLabelOptions().setExact(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Locator button(String caption) {
|
||||||
|
return page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(caption).setExact(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Locator notification() {
|
||||||
|
return page.locator("vaadin-notification-card");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.example.e2e;
|
||||||
|
|
||||||
|
import com.example.Application;
|
||||||
|
import com.microsoft.playwright.Browser;
|
||||||
|
import com.microsoft.playwright.BrowserContext;
|
||||||
|
import com.microsoft.playwright.BrowserType;
|
||||||
|
import com.microsoft.playwright.Page;
|
||||||
|
import com.microsoft.playwright.Playwright;
|
||||||
|
import com.microsoft.playwright.assertions.PlaywrightAssertions;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Assumptions;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for end-to-end tests that drive the running application in a real
|
||||||
|
* browser. The Spring Boot app is started on a random port, Playwright opens a
|
||||||
|
* headless Chromium against it, and every test gets a fresh browser context —
|
||||||
|
* so each test also gets a fresh Vaadin session.
|
||||||
|
*
|
||||||
|
* <p>Playwright downloads its browsers into {@code ~/.cache/ms-playwright} on
|
||||||
|
* first use. If that is not possible (offline runner, missing system libraries)
|
||||||
|
* the tests are <em>skipped</em> rather than failed, so the build stays green
|
||||||
|
* on runners that cannot host a browser.
|
||||||
|
*/
|
||||||
|
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
public abstract class PlaywrightTestBase {
|
||||||
|
|
||||||
|
/** Generous: the first request in dev mode has to bootstrap the frontend. */
|
||||||
|
private static final double TIMEOUT_MS = 60_000;
|
||||||
|
|
||||||
|
private static Playwright playwright;
|
||||||
|
private static Browser browser;
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
private BrowserContext context;
|
||||||
|
protected Page page;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void launchBrowser() {
|
||||||
|
try {
|
||||||
|
playwright = Playwright.create();
|
||||||
|
browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true));
|
||||||
|
} catch (Exception | UnsatisfiedLinkError e) {
|
||||||
|
closeBrowser();
|
||||||
|
Assumptions.abort("no Playwright browser available: " + e.getMessage());
|
||||||
|
}
|
||||||
|
PlaywrightAssertions.setDefaultAssertionTimeout(TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void closeBrowser() {
|
||||||
|
if (browser != null) {
|
||||||
|
browser.close();
|
||||||
|
browser = null;
|
||||||
|
}
|
||||||
|
if (playwright != null) {
|
||||||
|
playwright.close();
|
||||||
|
playwright = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void openPage() {
|
||||||
|
// Pin the locale so the assertions have one fixed set of captions to
|
||||||
|
// match. English, not German: the German bundle is the unsuffixed
|
||||||
|
// fallback (`translations.properties`), so `de` is not a provided
|
||||||
|
// locale and Vaadin serves `translations_en.properties` for it anyway.
|
||||||
|
context = browser.newContext(new Browser.NewContextOptions().setLocale(Locale.US.toLanguageTag()));
|
||||||
|
context.setDefaultTimeout(TIMEOUT_MS);
|
||||||
|
page = context.newPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void closePage() {
|
||||||
|
if (context != null) {
|
||||||
|
context.close();
|
||||||
|
context = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens a Vaadin route, e.g. {@code navigate("formular")}. */
|
||||||
|
protected void navigate(String route) {
|
||||||
|
page.navigate("http://localhost:" + port + "/" + route);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user