From 227959c9908cae620222de09038aec139fa9a790 Mon Sep 17 00:00:00 2001 From: Mikael Grankvist Date: Mon, 23 Mar 2026 13:54:19 +0200 Subject: [PATCH 01/22] feat: add initial loadtest to testbench --- pom.xml | 1 + vaadin-testbench-loadtest/.gitignore | 3 + vaadin-testbench-loadtest/.mcp.json | 14 + vaadin-testbench-loadtest/CLAUDE.md | 90 +++ vaadin-testbench-loadtest/README.md | 252 ++++++++ .../demo-web-app-loadtest/README.md | 212 +++++++ .../load-tests/demo-web-app-loadtest/pom.xml | 340 ++++++++++ .../load-tests/demo-web-app/README.md | 74 +++ .../load-tests/demo-web-app/pom.xml | 223 +++++++ .../com/vaadin/laboratory/Application.java | 42 ++ .../com/vaadin/laboratory/VaadinActuator.java | 74 +++ .../laboratory/data/AbstractEntity.java | 52 ++ .../vaadin/laboratory/data/SamplePerson.java | 78 +++ .../data/SamplePersonRepository.java | 11 + .../vaadin/laboratory/data/package-info.java | 4 + .../services/SamplePersonService.java | 44 ++ .../laboratory/services/package-info.java | 4 + .../vaadin/laboratory/views/MainLayout.java | 96 +++ .../views/crudexample/CrudExampleFactory.java | 27 + .../views/crudexample/CrudExampleView.java | 70 +++ .../views/crudexample/PersonForm.java | 139 +++++ .../views/crudexample/PersonGrid.java | 62 ++ .../views/helloworld/HelloWorldView.java | 35 ++ .../resources/META-INF/resources/favicon.ico | Bin 0 -> 1066 bytes .../META-INF/resources/icons/icon.png | Bin 0 -> 2424 bytes .../src/main/resources/application.properties | 23 + .../src/main/resources/banner.txt | 6 + .../demo-web-app/src/main/resources/data.sql | 100 +++ .../vaadin/laboratory/views/AbstractIT.java | 96 +++ .../views/scenario/CrudExampleIT.java | 129 ++++ .../views/scenario/HelloWorldIT.java | 52 ++ .../src/test/resources/logback-test.xml | 9 + vaadin-testbench-loadtest/load-tests/pom.xml | 32 + .../loadtest-error-handler/pom.xml | 53 ++ .../loadtest/LoadTestErrorHandler.java | 41 ++ .../loadtest/LoadTestServiceInitListener.java | 31 + ...adin.flow.server.VaadinServiceInitListener | 1 + vaadin-testbench-loadtest/pom.xml | 89 +++ .../testbench-converter-plugin/README.md | 144 +++++ .../testbench-converter-plugin/pom.xml | 111 ++++ .../testbench/loadtest/AbstractK6Mojo.java | 137 ++++ .../testbench/loadtest/K6ConvertMojo.java | 134 ++++ .../testbench/loadtest/K6RecordMojo.java | 412 ++++++++++++ .../vaadin/testbench/loadtest/K6RunMojo.java | 402 ++++++++++++ .../loadtest/util/ActuatorMetrics.java | 319 ++++++++++ .../testbench/loadtest/util/HarFilter.java | 274 ++++++++ .../loadtest/util/HarToK6Converter.java | 544 ++++++++++++++++ .../loadtest/util/K6ScenarioCombiner.java | 169 +++++ .../loadtest/util/K6TestRefactorer.java | 590 ++++++++++++++++++ .../loadtest/util/MetricsCollector.java | 334 ++++++++++ .../testbench/loadtest/util/NodeRunner.java | 268 ++++++++ .../loadtest/util/ProxyRecorder.java | 121 ++++ .../loadtest/util/ResourceExtractor.java | 92 +++ .../testbench/loadtest/util/SourceHasher.java | 123 ++++ .../resources/k6-utils/vaadin-k6-helpers.js | 200 ++++++ .../testbench-loadtest-support/README.md | 134 ++++ .../testbench-loadtest-support/pom.xml | 59 ++ .../testbench/loadtest/Destructive.java | 31 + .../loadtest/K6RecordingExtension.java | 83 +++ .../org.junit.jupiter.api.extension.Extension | 1 + 60 files changed, 7291 insertions(+) create mode 100644 vaadin-testbench-loadtest/.gitignore create mode 100644 vaadin-testbench-loadtest/.mcp.json create mode 100644 vaadin-testbench-loadtest/CLAUDE.md create mode 100644 vaadin-testbench-loadtest/README.md create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-loadtest/README.md create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-loadtest/pom.xml create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/README.md create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/pom.xml create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/Application.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/VaadinActuator.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/AbstractEntity.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/SamplePerson.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/SamplePersonRepository.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/package-info.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/services/SamplePersonService.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/services/package-info.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/MainLayout.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleFactory.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleView.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/PersonForm.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/PersonGrid.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/helloworld/HelloWorldView.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/resources/META-INF/resources/favicon.ico create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/resources/META-INF/resources/icons/icon.png create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/resources/application.properties create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/resources/banner.txt create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/resources/data.sql create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/test/java/com/vaadin/laboratory/views/AbstractIT.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/test/java/com/vaadin/laboratory/views/scenario/CrudExampleIT.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/test/java/com/vaadin/laboratory/views/scenario/HelloWorldIT.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/test/resources/logback-test.xml create mode 100644 vaadin-testbench-loadtest/load-tests/pom.xml create mode 100644 vaadin-testbench-loadtest/loadtest-error-handler/pom.xml create mode 100644 vaadin-testbench-loadtest/loadtest-error-handler/src/main/java/com/vaadin/testbench/loadtest/LoadTestErrorHandler.java create mode 100644 vaadin-testbench-loadtest/loadtest-error-handler/src/main/java/com/vaadin/testbench/loadtest/LoadTestServiceInitListener.java create mode 100644 vaadin-testbench-loadtest/loadtest-error-handler/src/main/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener create mode 100644 vaadin-testbench-loadtest/pom.xml create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/README.md create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/pom.xml create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/AbstractK6Mojo.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6ConvertMojo.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6RecordMojo.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6RunMojo.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/ActuatorMetrics.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarFilter.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/HarToK6Converter.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/K6ScenarioCombiner.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/K6TestRefactorer.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/MetricsCollector.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/NodeRunner.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/ProxyRecorder.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/ResourceExtractor.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/SourceHasher.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/resources/k6-utils/vaadin-k6-helpers.js create mode 100644 vaadin-testbench-loadtest/testbench-loadtest-support/README.md create mode 100644 vaadin-testbench-loadtest/testbench-loadtest-support/pom.xml create mode 100644 vaadin-testbench-loadtest/testbench-loadtest-support/src/main/java/com/vaadin/testbench/loadtest/Destructive.java create mode 100644 vaadin-testbench-loadtest/testbench-loadtest-support/src/main/java/com/vaadin/testbench/loadtest/K6RecordingExtension.java create mode 100644 vaadin-testbench-loadtest/testbench-loadtest-support/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension diff --git a/pom.xml b/pom.xml index 0e14b58ce..384b1e3d2 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,7 @@ vaadin-testbench-unit-junit5 vaadin-testbench-unit-junit6 vaadin-testbench-unit-quarkus + vaadin-testbench-loadtest diff --git a/vaadin-testbench-loadtest/.gitignore b/vaadin-testbench-loadtest/.gitignore new file mode 100644 index 000000000..8c642d938 --- /dev/null +++ b/vaadin-testbench-loadtest/.gitignore @@ -0,0 +1,3 @@ +*/src/main/frontend/generated +*/target +src/main/frontend/generated diff --git a/vaadin-testbench-loadtest/.mcp.json b/vaadin-testbench-loadtest/.mcp.json new file mode 100644 index 000000000..fb1e4f321 --- /dev/null +++ b/vaadin-testbench-loadtest/.mcp.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "Vaadin": { + "type": "http", + "url": "https://mcp.vaadin.com/docs" + }, + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} diff --git a/vaadin-testbench-loadtest/CLAUDE.md b/vaadin-testbench-loadtest/CLAUDE.md new file mode 100644 index 000000000..0862a59b6 --- /dev/null +++ b/vaadin-testbench-loadtest/CLAUDE.md @@ -0,0 +1,90 @@ +# AI TOOL GUIDANCE + +This file provides guidance when working with code in this repository. + +## Technology Stack + +This is a Vaadin application built with: +- Java +- Spring Boot +- Spring Data JPA with H2 database +- Maven build system + +## Development Commands + +### Running the Application +```bash +./mvnw # Start in development mode (default goal: spring-boot:run) +./mvnw spring-boot:run # Explicit development mode +``` + +The application will be available at http://localhost:8080 + +### Building for Production +```bash +./mvnw -Pproduction package # Build production JAR +docker build -t my-application:latest . # Build Docker image +``` + +### Testing +```bash +./mvnw test # Run all tests +./mvnw test -Dtest=TaskServiceTest # Run a single test class +./mvnw test -Dtest=TaskServiceTest#tasks_are_stored_in_the_database_with_the_current_timestamp # Run a single test method +``` + +## Architecture + +This project follows a **feature-based package structure** rather than traditional layered architecture. Code is organized by functional units (features), not by technical layers. + +### Package Structure + +- **`com.example.application.base`**: Reusable components and base classes for all features + - `base.ui.MainLayout`: AppLayout with drawer navigation using SideNav, automatically populated from @Menu annotations + - `base.ui.component.ViewToolbar`: Reusable toolbar component for views + +- **`com.example.application.examplefeature`**: Example feature demonstrating the structure + - `Task.java`: JPA entity with validation + - `TaskRepository.java`: Spring Data JPA repository + - `TaskService.java`: Service layer with @Transactional methods + - `ui.TaskListView.java`: Vaadin Flow view component (server-side UI) + - `TaskServiceTest.java`: Integration test using @SpringBootTest + +- **`Application.java`**: Main entry point, annotated with @SpringBootApplication and @Theme("default") + +### Key Architecture Patterns + +1. **Feature Packages**: Each feature is self-contained with its own UI, business logic, data access, and tests +2. **Navigation**: Views use `@Route` and `@Menu` annotations. MainLayout automatically builds navigation from menu entries +3. **Service Layer**: Use `@Transactional` for write operations and `@Transactional(readOnly = true)` for read operations +4. **Validation**: Domain validation in entity setters (see Task.setDescription) +5. **Dependency Injection**: Constructor injection throughout (no @Autowired on fields) + +## Adding New Features + +When creating a new feature: +1. Create a new package under `com.example.application` (e.g., `com.example.application.myfeature`) +2. Include: Entity, Repository, Service, and UI view classes +3. Use the `examplefeature` package as a reference +4. Once your features are complete, **delete the `examplefeature` package entirely** + +## Vaadin-Specific Notes + +- **Server-side rendering**: UI components are Java classes extending Vaadin components +- **Grid lazy loading**: Use `VaadinSpringDataHelpers.toSpringPageRequest(query)` for pagination +- **Themes**: Located in `src/main/frontend/themes/default/`, based on Lumo theme +- **Routing**: `@Route("")` for root path, `@Route("path")` for specific paths +- **Menu**: `@Menu` annotation controls navigation items (order, icon, title) + +## Database + +- H2 in-memory database for development +- JPA entities use `@GeneratedValue(strategy = GenerationType.SEQUENCE)` +- Entity equality based on ID (see Task.equals/hashCode pattern) + +## Testing + +- k6 tests are located in `src/test/k6` +- k6 tests can be run with `k6 run src/test/k6/...` +- https://github.com/johannest/k6-demo/blob/main/book-store.js is a good example of a k6 test +- https://grafana.com/docs/k6/latest/ is a good resource for learning k6 diff --git a/vaadin-testbench-loadtest/README.md b/vaadin-testbench-loadtest/README.md new file mode 100644 index 000000000..3a4189c66 --- /dev/null +++ b/vaadin-testbench-loadtest/README.md @@ -0,0 +1,252 @@ +# Vaadin LoadTestKit k6 - record real browser interactions and replay them at scale + +This is a PoC of a tool that utilizes Vaadin TestBench E2E tests as "user stories" for k6 based load testing. Technically the tooling starts a local instance of your server, records the web traffic of E2E test cases (you can select n+1 from your app), converts them into k6 scripts (taking details of Vaadin client-server communications, such as csrf preventation mechanism into account) and provides an easy way to then generate k6 load test against your test server (doesn't have to, and for meaningful numbers, shouldn't be the same server.) + + +## Project Structure + +``` +k6-testbench-recorder/ +├── testbench-converter-plugin/ # Maven plugin for k6 recording and conversion +├── demo-web-app/ # Sample Vaadin application with TestBench tests +└── demo-web-app-loadtest/ # Integration test module demonstrating the workflow +``` + +## Prerequisites + +| Tool | Version | Installation | +|------|---------|--------------| +| Java | 21+ | [Download](https://adoptium.net/) | +| Maven | 3.9+ | [Download](https://maven.apache.org/) | +| k6 | latest | `brew install k6` (macOS) or [Download](https://grafana.com/docs/k6/) | +| Chrome | latest | [Download](https://www.google.com/chrome/) | + +## Quick Start + +The modules are not pushed to Maven central yet, so make a clean install first: + +```bash +mvn install +``` + +Builds both the tooling and a simple demo web app with two Vaadin TestBench E2E tests. + +### Run the Demo (Local) + +*Note, you should not do this for anything else but to test the setup without external server* + +```bash + +# Run the complete workflow (start app, record, run load test) +mvn verify -pl demo-web-app-loadtest +``` + +### Option 3: Remote Load Testing + +Run pre-recorded tests against a server running on another machine: + +First deploy the test app to a remote server. The next snippet assumes the remote server is staging.example.com (replace to yours): + +```bash +# Test against a staging server +mvn verify -pl demo-web-app-loadtest -Premote \ + -Dk6.appHost=staging.example.com \ + -Dk6.appPort=8080 \ + -Dk6.vus=100 \ + -Dk6.duration=5m +``` + +This now re-builds the k6 tests against local server deployment if source tests (TestBench) have changed, and then executes load test with k6 against the defined appHost. + +### Using the plugin "manually" + +The `testbench-converter-plugin` plugin provides three goals, these are used in the loadtest module, but can be in theory used standalone as well. + +```bash +# Convert an existing HAR file to k6 +mvn k6:convert -Dk6.harFile=recording.har + +# Record a TestBench test and convert to k6 +mvn k6:record -Dk6.testClass=HelloWorldIT + +# Run a k6 load test +mvn k6:run -Dk6.testFile=k6/tests/hello-world.js -Dk6.vus=50 -Dk6.duration=1m +``` + +### JBang App (Alternative to Maven Plugin) + +For quick experimentation or when you don't want to use Maven for load testing orchestration, there's a standalone JBang app that provides the same recording functionality. + +Check [jbang/README.md](jbang/README.md) for documentation & demo walkthrough. + +The JBang app also serves as a **reference implementation** for creating your own custom tooling. + +## Creating TestBench Tests + +Standard TestBench integration tests define user workflows. The same that you use +already to ensure the functionality doesn't break. If you update the app and/or +your test pattern, your load tests are automatically updated 🥳 + +In practice you probably want to select certain case from your apps E2E test battery, or craft a special case(s) re-using some page-object classes of your E2E +tests. + +The k6:record goal runs these through a proxy to capture HTTP traffic for load testing. This is automated in the demo-web-app-loadtest module. + +*In the current PoC, we need a slight hacks to the superclass, but final version of the project should require no special things in your app (we'll find a workaround or add support to TestBench superclass).* + +### Example Test + +```java +public class HelloWorldIT extends AbstractIT { + + @BrowserTest + public void helloWorldWorkflow() { + // Enter name in text field + TextFieldElement nameField = $(TextFieldElement.class).first(); + nameField.setValue("Test User"); + + // Click button + $(ButtonElement.class).first().click(); + + // Verify result + $(NotificationElement.class).waitForFirst(); + } + + @Override + public String getViewName() { + return ""; // Root path + } +} +``` + +## Running k6 Tests + +Running k6 tests is automated in the demo-web-app-loadtest module and you can +tune parameter via Maven build file. The Maven example also displays a bit for +server health metrics after the execution, collected using Spring Boot Actuator. +Below you can see an example output: + + + + +The k6 scripts are also available in target directory if you want to execute them manually. + +### Basic Execution + +```bash +k6 run k6/tests/hello-world.js +``` + +### Load Testing + +```bash +# 50 virtual users for 30 seconds +k6 run --vus 50 --duration 30s k6/tests/hello-world.js + +# Against a different server +k6 run -e APP_IP=192.168.1.100 -e APP_PORT=8080 k6/tests/hello-world.js +``` + +### Understanding k6 Output + +``` + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ + + scenarios: (100.00%) 1 scenario, 50 max VUs, 1m30s max duration + + ✓ page load status equals 200 + ✓ vaadin init status equals 200 + + http_req_duration..............: avg=45.23ms min=12.34ms max=234.56ms + http_req_failed................: 0.00% ✓ 0 ✗ 1234 + http_reqs......................: 1234 41.13/s +``` + +Key metrics: +- **http_req_duration**: Response time (avg, min, max) +- **http_req_failed**: Percentage of failed requests +- **http_reqs**: Total requests and throughput + +## Module Documentation + +- [testbench-converter-plugin](testbench-converter-plugin/README.md) - Maven plugin documentation +- [demo-web-app](demo-web-app/README.md) - Sample application and scenarios +- [demo-web-app-loadtest](demo-web-app-loadtest/README.md) - Integration test workflow + +## How It Works + +The plugin uses pure Java utilities (no Node.js required) to: + +1. **Record** - BrowserMob Proxy captures browser traffic as HAR +2. **Filter** - Removes external requests (Google, analytics, etc.) +3. **Convert** - Generates k6 script from HAR +4. **Refactor** - Adds Vaadin-specific session handling: + - Dynamic JSESSIONID extraction + - CSRF token handling + - UI ID and Push ID management + - Configurable target server + - Realistic think time between user actions + +## Realistic User Simulation + +By default, the generated k6 scripts include realistic "think time" delays to simulate actual user behavior: + +- **Page read delay**: 2-5 seconds after page loads (user reading the page) +- **Interaction delay**: 0.5-2 seconds between user actions (thinking time) + +### How It Works + +The plugin intelligently analyzes HAR content and timing to identify user actions: + +1. **User action detection**: Analyzes UIDL request content to detect user interactions: + - Click events (button clicks, selections) + - Text input events (typing in fields) +2. **Page load detection**: Identifies v-r=init requests followed by resource loading as "page load" sequences +3. **Smart delay placement**: + - After page load completes: page read delay (user reading the page) + - After each user action: interaction delay (user thinking before next action) +4. **HAR timing awareness**: If the recorded HAR already has large gaps (> 500ms) - for example from `Thread.sleep()` or TestBench wait methods - no additional delay is added for that action + +### Configuration + +Configure think times via Maven properties: + +```xml + + + true + + + + 2.0 + + + + 0.5 + +``` + +Or via command line: + +```bash +# Disable think times for maximum throughput testing +mvn k6:record -Dk6.thinkTime.enabled=false + +# Custom delays +mvn k6:record -Dk6.thinkTime.pageReadDelay=3.0 -Dk6.thinkTime.interactionDelay=1.0 +``` + +### When to Disable Think Times + +Disable think times (`-Dk6.thinkTime.enabled=false`) when: +- **Maximum throughput testing**: You want to stress test the server at maximum request rate +- **TestBench tests with realistic sleeps**: If your TestBench tests already include realistic pauses using `Thread.sleep()` or TestBench wait methods, the HAR recording captures these delays. The plugin respects existing delays, but you may want to disable additional think times entirely + +## Useful Links + +- [Vaadin TestBench Documentation](https://vaadin.com/docs/latest/flow/testing) +- [k6 Documentation](https://grafana.com/docs/k6/latest/) diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app-loadtest/README.md b/vaadin-testbench-loadtest/load-tests/demo-web-app-loadtest/README.md new file mode 100644 index 000000000..14ee137a6 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-loadtest/README.md @@ -0,0 +1,212 @@ +# Demo Web Application Load Tests + +This module provides k6 load testing capabilities for the demo application in the `demo-web-app` module. It creates k6 scripts based on Vaadin TestBench integration tests and executes load tests against the server. It supports two main workflows through Maven profiles: + +1. **Local Development** (default in this demo, but not recommended for real projects) - Creates test scripts, starts the web server, and runs tests on the same machine +2. **Remote Load Testing** - Runs tests against a server running elsewhere. This is the recommended approach for real-world use: run load tests on representative hardware and generate traffic from a separate machine + +## Quick Start + +### Local Development Workflow + +Record scenarios and run quick load tests locally: + +```bash +# Build and run the full workflow (start server, record, run test) +mvn verify +``` + +### Remote Load Testing + +Run pre-recorded tests against a remote server: + +```bash +# Test against a staging server +mvn verify -Premote -Dk6.appHost=staging.example.com -Dk6.appPort=8080 + +# High-load test against production +mvn verify -Premote -Dk6.appHost=10.0.1.50 -Dk6.vus=100 -Dk6.duration=5m +``` + +## Maven Profiles + +### `local` (Default) + +The default profile for development and CI. It: + +1. Starts the demo-web-app locally +2. Records TestBench scenarios through a proxy +3. Runs a quick load test to verify the recording +4. Stops the application + +```bash +mvn verify # Full workflow +mvn verify -Dk6.skipRun=true # Only record, don't run load test +mvn verify -Dk6.skipRecord=true # Only run test, don't re-record +``` + +### `remote` + +For production load testing against a server running on another machine. This profile: + +- Does NOT start any server +- Validates the remote server is accessible +- Runs pre-recorded k6 tests against the configured target +- Is designed to run from a dedicated load generation machine + +```bash +# Basic usage +mvn verify -Premote -Dk6.appHost=staging.example.com + +# Full configuration +mvn verify -Premote \ + -Dk6.appHost=192.168.1.100 \ + -Dk6.appPort=8080 \ + -Dk6.vus=50 \ + -Dk6.duration=2m +``` + +### `record-only` + +Records scenarios without running load tests. Useful for preparing tests that will be executed later on dedicated infrastructure. + +```bash +mvn verify -Precord-only +``` + +## Included Scenarios + +This demo records and runs two TestBench integration tests as k6 load tests: + +| Scenario | Description | Generated Test | +|----------|-------------|----------------| +| `HelloWorldIT` | Simple form interaction: enter name, click button | `hello-world.js` | +| `CrudExampleIT` | Full CRUD workflow: browse grid, create, edit, delete | `crud-example.js` | + +## Configuration Properties + +| Property | Default | Description | +|----------|---------|-------------| +| `k6.appHost` | `localhost` | Target server hostname/IP (remote profile) | +| `k6.appPort` | `8080` | Target server port (remote profile) | +| `app.port` | `8081` | Local server port (local profile) | +| `proxy.port` | `6000` | Recording proxy port | +| `k6.vus` | `10` | Number of virtual users | +| `k6.duration` | `30s` | Test duration (e.g., "30s", "1m", "5m") | +| `k6.testDir` | `target/k6/tests` | Directory containing k6 test files | +| `k6.skipRecord` | `false` | Skip recording phase | +| `k6.skipRun` | `false` | Skip load test phase | + +## Production Load Testing Best Practices + +For accurate performance metrics, follow these guidelines: + +### 1. Separate Machines + +Run the load generator on a different machine than the application server: + +``` +┌─────────────────┐ ┌─────────────────┐ +│ Load Generator │ ───► │ App Server │ +│ (this module) │ HTTP │ (demo-web-app) │ +│ mvn -Premote │ │ java -jar ... │ +└─────────────────┘ └─────────────────┘ +``` + +### 2. Network Considerations + +- Use a fast, stable network connection +- Consider network latency in your test results +- For cloud testing, run load generator in same region as app server + +### 3. Pre-Record Tests + +Record tests locally, then distribute the k6 scripts: + +```bash +# On development machine: record the test +mvn verify -Precord-only + +# Copy k6/tests/*.js to load generation machine + +# On load generation machine: run the test +mvn verify -Premote -Dk6.appHost=app-server.example.com +``` + +### 4. Scaling Up + +Increase load gradually to find breaking points: + +```bash +# Start small +mvn verify -Premote -Dk6.appHost=prod -Dk6.vus=10 -Dk6.duration=1m + +# Increase load +mvn verify -Premote -Dk6.appHost=prod -Dk6.vus=50 -Dk6.duration=5m + +# Stress test +mvn verify -Premote -Dk6.appHost=prod -Dk6.vus=200 -Dk6.duration=10m +``` + +## Example: CI/CD Integration + +### GitHub Actions (Remote Testing) + +```yaml +jobs: + load-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Install k6 + run: | + sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 + echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list + sudo apt-get update + sudo apt-get install k6 + + - name: Run Load Test + run: | + mvn verify -pl demo-web-app-loadtest -Premote \ + -Dk6.appHost=${{ secrets.STAGING_HOST }} \ + -Dk6.appPort=8080 \ + -Dk6.vus=50 \ + -Dk6.duration=2m +``` + +## Troubleshooting + +### Remote server not accessible + +``` +ERROR: Cannot reach http://staging.example.com:8080 +``` + +- Verify the server is running and accessible +- Check firewall rules allow traffic on the specified port +- Ensure correct hostname/IP and port + +### k6 not found + +``` +k6 is required but not found +``` + +k6 must be installed on the machine generating the load. For testing your setup locally, you can also install it on your workstation. + +Install k6: +- macOS: `brew install k6` +- Linux: See [k6 installation guide](https://grafana.com/docs/k6/latest/get-started/installation/) + +### Recording fails + +- Check the proxy port (6000) is not in use +- Verify the application is running and accessible +- Ensure Chrome browser is installed (required for TestBench) diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app-loadtest/pom.xml b/vaadin-testbench-loadtest/load-tests/demo-web-app-loadtest/pom.xml new file mode 100644 index 000000000..b4656f668 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-loadtest/pom.xml @@ -0,0 +1,340 @@ + + + 4.0.0 + + + com.vaadin + load-tests + 10.1-SNAPSHOT + + + demo-web-app-loadtest + Demo Web Application Load Tests + pom + + + K6 load testing module for the demo Vaadin application. + + Usage modes: + 1. Development (default): Starts server locally, records scenarios, runs quick test + mvn verify + + 2. Remote load testing: Runs pre-recorded tests against a remote server + mvn verify -Premote -Dk6.appHost=staging.example.com -Dk6.appPort=8080 + + For production load testing, always run the load generator on a separate machine + from the application server to get accurate performance metrics. + + + + + 8081 + 8082 + 6000 + + + localhost + 8080 + + + 100 + 30s + ${project.build.directory}/k6/tests + + + true + helloWorld:70,crudExample:30 + + + true + + + false + false + + + + + + com.vaadin + demo-web-app + ${project.version} + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.1 + + + copy-demo-app + package + + copy + + + + + com.vaadin + demo-web-app + ${project.version} + jar + demo-web-app.jar + + + ${project.build.directory} + + + + + + + + + + + local + + true + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.1 + + + + start-demo-app + pre-integration-test + + exec + + + java + + -jar + ${project.build.directory}/demo-web-app.jar + --server.port=${app.port} + --management.server.port=${management.port} + + true + true + + + + + + stop-demo-app + post-integration-test + + exec + + + bash + + -c + + echo "Stopping demo app..." + pkill -f "demo-web-app.jar.*--server.port=${app.port}" || true + + + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + wait-for-app + pre-integration-test + + run + + + + Waiting for health endpoint on port ${management.port}... + + + + Health endpoint ready. Waiting for app on port ${app.port}... + + + + App is ready on port ${app.port}! + + + + + + + + + com.vaadin + testbench-converter-plugin + ${project.version} + + + + record-scenarios + integration-test + + record + + + ${k6.skipRecord} + + HelloWorldIT + CrudExampleIT + + ${proxy.port} + ${app.port} + ${project.basedir}/../demo-web-app + ${project.build.directory} + ${project.build.directory}/k6/tests + + + + + + run-load-tests + integration-test + + run + + + ${k6.skipRun} + ${k6.testDir} + ${k6.vus} + ${k6.duration} + ${app.port} + ${management.port} + ${k6.combineScenarios} + ${k6.scenarioWeights} + ${k6.collectVaadinMetrics} + + + + + + + + + + + remote + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.1 + + + check-remote-server + pre-integration-test + + exec + + + bash + + -c + + echo "==============================================" + echo "Remote Load Test Configuration" + echo "==============================================" + echo " Target: http://${k6.appHost}:${k6.appPort}" + echo " Tests: ${k6.testDir}" + echo " VUs: ${k6.vus}" + echo " Duration: ${k6.duration}" + echo "==============================================" + echo "" + echo "Checking if remote server is accessible..." + if curl -s -o /dev/null --connect-timeout 10 "http://${k6.appHost}:${k6.appPort}"; then + echo "Remote server is accessible!" + else + echo "ERROR: Cannot reach http://${k6.appHost}:${k6.appPort}" + echo "Make sure the application is running on the target server." + exit 1 + fi + + + + + + + + + + com.vaadin + testbench-converter-plugin + ${project.version} + + + run-remote-load-tests + integration-test + + run + + + ${k6.testDir} + ${k6.vus} + ${k6.duration} + ${k6.appHost} + ${k6.appPort} + ${k6.combineScenarios} + ${k6.scenarioWeights} + ${k6.collectVaadinMetrics} + + + + + + + + + + + record-only + + true + + + + diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/README.md b/vaadin-testbench-loadtest/load-tests/demo-web-app/README.md new file mode 100644 index 000000000..5e12a9c21 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/README.md @@ -0,0 +1,74 @@ +# Demo Web Application + +A sample Vaadin application with Spring Boot, demonstrating common patterns like CRUD operations and form handling. + +## Running the Application + +```bash +mvn spring-boot:run +``` + +The application starts at http://localhost:8080 + +## Project Structure + +``` +src/main/java/com/vaadin/laboratory/ +├── Application.java # Spring Boot entry point +├── data/ # JPA entities and repositories +│ ├── SamplePerson.java +│ └── SamplePersonRepository.java +├── services/ # Business logic +│ └── SamplePersonService.java +└── views/ # Vaadin UI views + ├── MainLayout.java # Application layout with navigation + ├── helloworld/ + │ └── HelloWorldView.java # Simple hello world demo + └── crudexample/ + └── CrudExampleView.java # CRUD grid with form editing + +src/test/java/com/vaadin/laboratory/views/ +├── AbstractIT.java # Base class for integration tests +└── scenario/ + ├── HelloWorldIT.java # E2E test for HelloWorld view + └── CrudExampleIT.java # E2E test for CRUD view +``` + +## Views + +### Hello World +A simple view with a text field and button that displays a greeting notification. + +### CRUD Example +A master-detail view with a grid of sample persons and an editing form. Demonstrates: +- Lazy-loading grid with Spring Data +- Form binding with validation +- Create, update, and delete operations + +## Integration Tests + +The `scenario` package contains TestBench-based end-to-end tests that simulate real user interactions. These tests: + +- Verify the application works correctly from a user's perspective +- Can be run as standard integration tests +- Are also used by the `demo-web-app-loadtest` module to record user workflows for load testing + +### Running Tests + +```bash +# Run all integration tests +mvn verify + +# Run a specific test +mvn failsafe:integration-test -Dit.test=HelloWorldIT +``` + +## Building + +```bash +# Development build +mvn package + +# Production build with optimized frontend +mvn package -Pproduction +``` diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/pom.xml b/vaadin-testbench-loadtest/load-tests/demo-web-app/pom.xml new file mode 100644 index 000000000..2a8814176 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/pom.xml @@ -0,0 +1,223 @@ + + + 4.0.0 + + + com.vaadin + load-tests + 10.1-SNAPSHOT + + + demo-web-app + Demo Web Application + jar + + Demo Vaadin application with TestBench tests for k6 recording + + + + + + org.springframework.boot + spring-boot-dependencies + 4.0.1 + pom + import + + + + + + + com.vaadin + + vaadin + + + com.vaadin + vaadin-spring-boot-starter + + + org.parttio + line-awesome + 2.1.0 + + + com.vaadin + loadtest-error-handler + ${project.version} + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-actuator + + + com.vaadin + vaadin-dev + true + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin + vaadin-testbench-junit6 + test + + true + + + + com.vaadin + testbench-loadtest-support + ${project.version} + test + true + + + + + spring-boot:run + + + org.springframework.boot + spring-boot-maven-plugin + 4.0.1 + + + + repackage + + + + + + com.vaadin + vaadin-maven-plugin + ${vaadin.version} + + + + build-frontend + + + + + + + + + + it + + + + org.springframework.boot + spring-boot-maven-plugin + 4.0.1 + + + start-spring-boot + pre-integration-test + + start + + + + stop-spring-boot + post-integration-test + + stop + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + false + true + + + + + + + + + production + + + + com.vaadin + vaadin-dev + provided + + + + + + com.vaadin + vaadin-maven-plugin + ${vaadin.version} + + + + build-frontend + + compile + + + + true + + + + + + + diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/Application.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/Application.java new file mode 100644 index 000000000..0f39513b4 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/Application.java @@ -0,0 +1,42 @@ +package com.vaadin.laboratory; + +import com.vaadin.laboratory.data.SamplePersonRepository; +import com.vaadin.flow.component.dependency.StyleSheet; +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.theme.Theme; +import com.vaadin.flow.theme.lumo.Lumo; +import javax.sql.DataSource; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jdbc.autoconfigure.ApplicationDataSourceScriptDatabaseInitializer; +import org.springframework.boot.sql.autoconfigure.init.SqlInitializationProperties; +import org.springframework.context.annotation.Bean; + +/** + * The entry point of the Spring Boot application. + * + * Use the @PWA annotation make the application installable on phones, tablets + * and some desktop browsers. + * + */ +@SpringBootApplication +@StyleSheet(Lumo.STYLESHEET) +@EnableConfigurationProperties(SqlInitializationProperties.class) +public class Application implements AppShellConfigurator { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + @Bean + ApplicationDataSourceScriptDatabaseInitializer customInitializer(DataSource dataSource, + SqlInitializationProperties properties, SamplePersonRepository repository) { + // Only run schema.sql/data.sql when the DB is empty + return new ApplicationDataSourceScriptDatabaseInitializer(dataSource, properties) { + @Override + public boolean initializeDatabase() { + return (repository.count() == 0L) && super.initializeDatabase(); + } + }; + } +} diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/VaadinActuator.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/VaadinActuator.java new file mode 100644 index 000000000..037ff5247 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/VaadinActuator.java @@ -0,0 +1,74 @@ +package com.vaadin.laboratory; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.ServiceInitEvent; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.ApplicationScope; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Exposes bit of stats from the Vaadin app via Spring Boot's Actuator mechanism. + */ +@Component +@ApplicationScope +public class VaadinActuator { + + private final Set activeUis = Collections.synchronizedSet(new HashSet<>()); + private final Map viewToCount = Collections.synchronizedMap(new HashMap<>()); + private final MeterRegistry meterRegistry; + + public VaadinActuator(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + // Register a gauge metric for active UI count + Gauge.builder("vaadin.view.count", activeUis::size) + .description("Number of active Vaadin UI instances") + .register(meterRegistry); + } + + @EventListener + public void onServiceInit(ServiceInitEvent serviceInitEvent) { + + System.err.println("Starting UI registering...."); + + serviceInitEvent.getSource().addUIInitListener(event -> { + UI ui = event.getUI(); + + ui.addAfterNavigationListener(navEvt -> { + activeUis.add(ui); + com.vaadin.flow.component.Component currentView = ui.getCurrentView(); + Class aClass = currentView.getClass(); + // Get or create counter for this view type (thread-safe) + AtomicInteger viewCounter = viewToCount.computeIfAbsent(aClass, k -> { + AtomicInteger counter = new AtomicInteger(0); + // Register gauge for this specific view type + Gauge.builder("vaadin.view.count", counter::get) + .tag("view", aClass.getSimpleName()) + .description("Number of active " + aClass.getSimpleName() + " view instances") + .register(meterRegistry); + return counter; + }); + + viewCounter.incrementAndGet(); + + currentView.addDetachListener(event1 -> { + viewCounter.decrementAndGet(); + }); + + }); + + ui.addDetachListener(detachEvent -> { + activeUis.remove(ui); + }); + + }); + } + + public int getActiveUiCount() { + return activeUis.size(); + } +} diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/AbstractEntity.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/AbstractEntity.java new file mode 100644 index 000000000..ac5ce9180 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/AbstractEntity.java @@ -0,0 +1,52 @@ +package com.vaadin.laboratory.data; + +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Version; + +@MappedSuperclass +public abstract class AbstractEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "idgenerator") + // The initial value is to account for data.sql demo data ids + @SequenceGenerator(name = "idgenerator", initialValue = 1000) + private Long id; + + @Version + private int version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public int getVersion() { + return version; + } + + @Override + public int hashCode() { + if (getId() != null) { + return getId().hashCode(); + } + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AbstractEntity that)) { + return false; // null or not an AbstractEntity class + } + if (getId() != null) { + return getId().equals(that.getId()); + } + return super.equals(that); + } +} diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/SamplePerson.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/SamplePerson.java new file mode 100644 index 000000000..e8a0375bf --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/SamplePerson.java @@ -0,0 +1,78 @@ +package com.vaadin.laboratory.data; + +import jakarta.persistence.Entity; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Past; + +import java.time.LocalDate; + +@Entity +public class SamplePerson extends AbstractEntity { + + @NotEmpty + private String firstName; + @NotEmpty + private String lastName; + @NotEmpty + @Email + private String email; + private String phone; + @NotNull + @Past + private LocalDate dateOfBirth; + private String occupation; + private String role; + private boolean important; + + public String getFirstName() { + return firstName; + } + public void setFirstName(String firstName) { + this.firstName = firstName; + } + public String getLastName() { + return lastName; + } + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + public String getPhone() { + return phone; + } + public void setPhone(String phone) { + this.phone = phone; + } + public LocalDate getDateOfBirth() { + return dateOfBirth; + } + public void setDateOfBirth(LocalDate dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } + public String getOccupation() { + return occupation; + } + public void setOccupation(String occupation) { + this.occupation = occupation; + } + public String getRole() { + return role; + } + public void setRole(String role) { + this.role = role; + } + public boolean isImportant() { + return important; + } + public void setImportant(boolean important) { + this.important = important; + } + +} diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/SamplePersonRepository.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/SamplePersonRepository.java new file mode 100644 index 000000000..71f7c452a --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/SamplePersonRepository.java @@ -0,0 +1,11 @@ +package com.vaadin.laboratory.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface SamplePersonRepository + extends + JpaRepository, + JpaSpecificationExecutor { + +} diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/package-info.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/package-info.java new file mode 100644 index 000000000..aaa0debdf --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/data/package-info.java @@ -0,0 +1,4 @@ +@NonNullApi +package com.vaadin.laboratory.data; + +import org.springframework.lang.NonNullApi; diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/services/SamplePersonService.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/services/SamplePersonService.java new file mode 100644 index 000000000..249cf4abc --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/services/SamplePersonService.java @@ -0,0 +1,44 @@ +package com.vaadin.laboratory.services; + +import com.vaadin.laboratory.data.SamplePerson; +import com.vaadin.laboratory.data.SamplePersonRepository; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +@Service +public class SamplePersonService { + + private final SamplePersonRepository repository; + + public SamplePersonService(SamplePersonRepository repository) { + this.repository = repository; + } + + public Optional get(Long id) { + return repository.findById(id); + } + + public SamplePerson save(SamplePerson entity) { + return repository.save(entity); + } + + public void delete(Long id) { + repository.deleteById(id); + } + + public Page list(Pageable pageable) { + return repository.findAll(pageable); + } + + public Page list(Pageable pageable, Specification filter) { + return repository.findAll(filter, pageable); + } + + public int count() { + return (int) repository.count(); + } + +} diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/services/package-info.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/services/package-info.java new file mode 100644 index 000000000..dc79de7e6 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/services/package-info.java @@ -0,0 +1,4 @@ +@NonNullApi +package com.vaadin.laboratory.services; + +import org.springframework.lang.NonNullApi; diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/MainLayout.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/MainLayout.java new file mode 100644 index 000000000..a44cb6e8d --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/MainLayout.java @@ -0,0 +1,96 @@ +package com.vaadin.laboratory.views; + +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.html.Footer; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.Header; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.SvgIcon; +import com.vaadin.flow.component.orderedlayout.Scroller; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.AfterNavigationObserver; +import com.vaadin.flow.router.Layout; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.server.menu.MenuConfiguration; +import com.vaadin.flow.server.menu.MenuEntry; +import com.vaadin.flow.theme.lumo.LumoUtility; +import java.util.List; + +/** + * The main view is a top-level placeholder for other views. + */ +@Layout +@AnonymousAllowed +public class MainLayout extends AppLayout implements AfterNavigationObserver { + + private static final Logger logger = LoggerFactory.getLogger(MainLayout.class); + private static final AtomicInteger sessionCounter = new AtomicInteger(0); + + private H1 viewTitle; + + public MainLayout() { + int sessionId = sessionCounter.incrementAndGet(); + logger.debug("MainLayout created, session #{}", sessionId); + + setPrimarySection(Section.DRAWER); + addDrawerContent(); + addHeaderContent(); + } + + private void addHeaderContent() { + DrawerToggle toggle = new DrawerToggle(); + toggle.setAriaLabel("Menu toggle"); + + viewTitle = new H1(); + viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + + addToNavbar(true, toggle, viewTitle); + } + + private void addDrawerContent() { + Span appName = new Span("My App"); + appName.addClassNames(LumoUtility.FontWeight.SEMIBOLD, LumoUtility.FontSize.LARGE); + Header header = new Header(appName); + + Scroller scroller = new Scroller(createNavigation()); + + addToDrawer(header, scroller, createFooter()); + } + + private SideNav createNavigation() { + SideNav nav = new SideNav(); + + List menuEntries = MenuConfiguration.getMenuEntries(); + menuEntries.forEach(entry -> { + if (entry.icon() != null) { + nav.addItem(new SideNavItem(entry.title(), entry.path(), new SvgIcon(entry.icon()))); + } else { + nav.addItem(new SideNavItem(entry.title(), entry.path())); + } + }); + + return nav; + } + + private Footer createFooter() { + Footer layout = new Footer(); + + return layout; + } + + @Override + public void afterNavigation(AfterNavigationEvent event) { + viewTitle.setText(getCurrentPageTitle()); + } + + private String getCurrentPageTitle() { + return MenuConfiguration.getPageHeader(getContent()).orElse(""); + } +} diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleFactory.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleFactory.java new file mode 100644 index 000000000..006d9439d --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleFactory.java @@ -0,0 +1,27 @@ +package com.vaadin.laboratory.views.crudexample; + +import com.vaadin.laboratory.services.SamplePersonService; +import com.vaadin.flow.function.SerializableRunnable; +import org.springframework.stereotype.Component; + +@Component +class CrudExampleFactory { + + private final SamplePersonService samplePersonService; + + public CrudExampleFactory(SamplePersonService samplePersonService) { + this.samplePersonService = samplePersonService; + } + + PersonForm createForm(SerializableRunnable refreshGridRunnable) { + return new PersonForm(samplePersonService, refreshGridRunnable); + } + + SamplePersonService createService() { + return samplePersonService; + } + + public PersonGrid createGrid(SerializableRunnable clearForm) { + return new PersonGrid(samplePersonService, clearForm); + } +} diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleView.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleView.java new file mode 100644 index 000000000..a754423c5 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleView.java @@ -0,0 +1,70 @@ +package com.vaadin.laboratory.views.crudexample; + +import com.vaadin.laboratory.data.SamplePerson; +import com.vaadin.laboratory.services.SamplePersonService; +import com.vaadin.flow.component.dependency.Uses; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.splitlayout.SplitLayout; +import com.vaadin.flow.router.*; +import org.vaadin.lineawesome.LineAwesomeIconUrl; + +import java.util.Optional; + + +@PageTitle("Crud Example") +@Route(value = "crud-example/:samplePersonID?/:action?(edit)") +@Menu(order = 1, icon = LineAwesomeIconUrl.COLUMNS_SOLID) +@Uses(Icon.class) +public class CrudExampleView extends Div implements BeforeEnterObserver { + + static final String SAMPLEPERSON_ID = "samplePersonID"; + static final String SAMPLEPERSON_EDIT_ROUTE_TEMPLATE = "crud-example/%s/edit"; + + private final PersonGrid grid; + private final SamplePersonService samplePersonService; + private final PersonForm form; + + public CrudExampleView(CrudExampleFactory crudExampleFactory) { + addClassNames("crud-example-view"); + + samplePersonService = crudExampleFactory.createService(); + grid = crudExampleFactory.createGrid(this::clearForm); + form = crudExampleFactory.createForm(this::refreshGrid); + + // Create UI + SplitLayout splitLayout = new SplitLayout(); + splitLayout.addToPrimary(grid); + splitLayout.addToSecondary(form); + + add(splitLayout); + } + + private void refreshGrid() { + grid.refreshGrid(); + } + + private void clearForm() { + form.clearForm(); + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + Optional samplePersonId = event.getRouteParameters().get(SAMPLEPERSON_ID).map(Long::parseLong); + if (samplePersonId.isPresent()) { + Optional samplePersonFromBackend = samplePersonService.get(samplePersonId.get()); + if (samplePersonFromBackend.isPresent()) { + form.populateForm(samplePersonFromBackend.get()); + } else { + Notification.show( + String.format("The requested samplePerson was not found, ID = %s", samplePersonId.get()), 3000, + Notification.Position.BOTTOM_START); + // when a row is selected but the data is no longer available, + // refresh grid + this.refreshGrid(); + event.forwardTo(CrudExampleView.class); + } + } + } +} diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/PersonForm.java b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/PersonForm.java new file mode 100644 index 000000000..6ee18df7f --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app/src/main/java/com/vaadin/laboratory/views/crudexample/PersonForm.java @@ -0,0 +1,139 @@ +package com.vaadin.laboratory.views.crudexample; + +import com.vaadin.laboratory.data.SamplePerson; +import com.vaadin.laboratory.services.SamplePersonService; +import com.vaadin.flow.component.ClickEvent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.datepicker.DatePicker; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.ValidationException; +import com.vaadin.flow.function.SerializableRunnable; +import org.springframework.orm.ObjectOptimisticLockingFailureException; + +class PersonForm extends Div { + + private final TextField firstName = new TextField("First Name"); + private final TextField lastName = new TextField("Last Name"); + private final TextField email = new TextField("Email"); + private final TextField phone = new TextField("Phone"); + private final DatePicker dateOfBirth = new DatePicker("Date Of Birth"); + private final TextField occupation = new TextField("Occupation"); + private final TextField role = new TextField("Role"); + private final Checkbox important = new Checkbox("Important"); + + private final Button delete = new Button("", VaadinIcon.TRASH.create()); + private final Button cancel = new Button("Cancel"); + private final Button save = new Button("Save"); + + private final SamplePersonService samplePersonService; + private final SerializableRunnable refreshGridRunnable; + private final BeanValidationBinder binder; + private SamplePerson samplePerson; + + PersonForm(SamplePersonService samplePersonService, SerializableRunnable refreshGridRunnable) { + this.samplePersonService = samplePersonService; + this.refreshGridRunnable = refreshGridRunnable; + setClassName("editor-layout"); + + add(createFormLayout()); + add(createButtonLayout()); + + // Configure Form + binder = new BeanValidationBinder<>(SamplePerson.class); + + // Bind fields. This is where you'd define e.g. validation rules + binder.bindInstanceFields(this); + + save.addClickListener(this::clickSaveButton); + cancel.addClickListener(this::clickCancelButton); + delete.addClickListener(this::clickDeleteButton); + + add("test"); + } + + private void clickDeleteButton(ClickEvent