From adc0d3755f25c1aad3917863074e91fd95171809 Mon Sep 17 00:00:00 2001 From: Mikael Grankvist Date: Wed, 8 Apr 2026 09:54:13 +0300 Subject: [PATCH 1/7] feat: add playwright record goal Add goal for recording playwright test HAR files. Add demo module with playwright tests Add playwright loadtest module Have playwright tests under playwright-load-tests profile so one can run only testbench or playwright. --- .gitignore | 1 + vaadin-testbench-loadtest/README.md | 9 +- .../demo-web-app-playwright-loadtest/pom.xml | 282 ++++++++++++++++++ .../demo-web-app-playwright/README.md | 71 +++++ .../demo-web-app-playwright/pom.xml | 207 +++++++++++++ .../com/vaadin/laboratory/Application.java | 54 ++++ .../laboratory/data/AbstractEntity.java | 60 ++++ .../vaadin/laboratory/data/SamplePerson.java | 101 +++++++ .../data/SamplePersonRepository.java | 18 ++ .../vaadin/laboratory/data/package-info.java | 4 + .../services/SamplePersonService.java | 55 ++++ .../laboratory/services/package-info.java | 4 + .../vaadin/laboratory/views/MainLayout.java | 109 +++++++ .../views/crudexample/CrudExampleFactory.java | 36 +++ .../views/crudexample/CrudExampleView.java | 85 ++++++ .../views/crudexample/PersonForm.java | 156 ++++++++++ .../views/crudexample/PersonGrid.java | 82 +++++ .../views/helloworld/HelloWorldView.java | 44 +++ .../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 + .../src/main/resources/data.sql | 100 +++++++ .../scenario/CrudExamplePlaywrightIT.java | 152 ++++++++++ .../scenario/HelloWorldPlaywrightIT.java | 57 ++++ .../load-tests/demo-web-app/pom.xml | 1 + .../src/test/resources/logback-test.xml | 9 - vaadin-testbench-loadtest/load-tests/pom.xml | 12 + .../loadtest-helper/pom.xml | 14 + .../loadtest/AbstractPlaywrightHelper.java | 120 ++++++++ .../testbench/loadtest/AbstractK6Mojo.java | 2 + ...ecordMojo.java => AbstractRecordMojo.java} | 275 ++++++++--------- .../loadtest/PlaywrightRecordMojo.java | 101 +++++++ .../loadtest/TestbenchRecordMojo.java | 122 ++++++++ 34 files changed, 2206 insertions(+), 166 deletions(-) create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright-loadtest/pom.xml create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/README.md create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/pom.xml create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/Application.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/AbstractEntity.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/SamplePerson.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/SamplePersonRepository.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/package-info.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/services/SamplePersonService.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/services/package-info.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/MainLayout.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleFactory.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleView.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/PersonForm.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/PersonGrid.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/helloworld/HelloWorldView.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/resources/META-INF/resources/favicon.ico create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/resources/META-INF/resources/icons/icon.png create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/resources/application.properties create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/resources/banner.txt create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/resources/data.sql create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/test/java/com/vaadin/laboratory/views/scenario/CrudExamplePlaywrightIT.java create mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/test/java/com/vaadin/laboratory/views/scenario/HelloWorldPlaywrightIT.java delete mode 100644 vaadin-testbench-loadtest/load-tests/demo-web-app/src/test/resources/logback-test.xml create mode 100644 vaadin-testbench-loadtest/loadtest-helper/src/main/java/com/vaadin/testbench/loadtest/AbstractPlaywrightHelper.java rename vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/{K6RecordMojo.java => AbstractRecordMojo.java} (58%) create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/PlaywrightRecordMojo.java create mode 100644 vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/TestbenchRecordMojo.java diff --git a/.gitignore b/.gitignore index f468b4753..0302ffe7c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ error-screenshots **/frontend/index.html **/vite.generated.ts +**/bundles/* diff --git a/vaadin-testbench-loadtest/README.md b/vaadin-testbench-loadtest/README.md index b7113bb4a..bbe3d6f38 100644 --- a/vaadin-testbench-loadtest/README.md +++ b/vaadin-testbench-loadtest/README.md @@ -8,8 +8,13 @@ This is a PoC of a tool that utilizes Vaadin TestBench E2E tests as "user storie ``` 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 +├── testbench-loadtest-support/ # A JUnit 5 extension +├── loadtest-helper/ # Drop-in helper for Vaadin load testing +└───── load-tests/ # Test module + ├── demo-web-app/ # Sample Vaadin application with TestBench tests + └── demo-web-app-loadtest/ # Integration test module demonstrating the workflow + ├── demo-web-app-playwright/ # Sample Vaadin application with Playwright tests + └── demo-web-app-playwright-loadtest/ # Integration test module demonstrating the workflow for playwright ``` ## Prerequisites diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright-loadtest/pom.xml b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright-loadtest/pom.xml new file mode 100644 index 000000000..8a8b6a5fe --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright-loadtest/pom.xml @@ -0,0 +1,282 @@ + + + 4.0.0 + + + com.vaadin + load-tests + 10.2-SNAPSHOT + + + demo-web-app-playwright-loadtest + Demo Web Application Playwright Load Tests + pom + + + K6 load testing module for the demo Vaadin application using Playwright. + + Usage modes: + 1. Development (default): Starts server locally, records Playwright 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 + + + localhost + 8080 + + + 100 + 30s + ${project.build.directory}/k6/tests + + + true + helloWorld:70,crudExample:30 + + + true + + + false + false + + + + + + com.vaadin + demo-web-app-playwright + ${project.version} + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.1 + + + copy-demo-app + package + + copy + + + + + com.vaadin + demo-web-app-playwright + ${project.version} + jar + demo-web-app-playwright.jar + + + ${project.build.directory} + + + + + + + + + + + local + + true + + + + + com.vaadin + testbench-converter-plugin + ${project.version} + + + + start-demo-app + + start-server + + + ${project.build.directory}/demo-web-app-playwright.jar + ${app.port} + ${management.port} + + + + + + record-scenarios + integration-test + + record-playwright + + + ${k6.skipRecord} + + HelloWorldPlaywrightIT + CrudExamplePlaywrightIT + + ${app.port} + ${project.basedir}/../demo-web-app-playwright + ${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} + + + + + + stop-demo-app + + stop-server + + + + + + + + + + + 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-playwright/README.md b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/README.md new file mode 100644 index 000000000..c42c7fafb --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/README.md @@ -0,0 +1,71 @@ +# Demo Web Application (Playwright) + +A sample Vaadin application with Spring Boot, demonstrating common patterns like CRUD operations and form handling. Uses Playwright for integration tests instead of TestBench. + +## 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/scenario/ +├── HelloWorldPlaywrightIT.java # E2E test for HelloWorld view +└── CrudExamplePlaywrightIT.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 Playwright-based end-to-end tests that simulate real user interactions. These tests extend `AbstractPlaywrightHelper` from the `loadtest-helper` module, which provides: + +- Managed Playwright lifecycle (browser, context, page) +- Automatic HAR recording when run via the `loadtest:record-playwright` Maven goal + +### Running Tests + +```bash +# Run all integration tests (requires the app to be running) +mvn failsafe:integration-test -Dit.test=HelloWorldPlaywrightIT + +# Run with the it profile (starts/stops the app automatically) +mvn verify -Pit +``` + +## 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-playwright/pom.xml b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/pom.xml new file mode 100644 index 000000000..cc0c8c619 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/pom.xml @@ -0,0 +1,207 @@ + + + 4.0.0 + + + com.vaadin + load-tests + 10.2-SNAPSHOT + + + demo-web-app-playwright + Demo Web Application (Playwright) + jar + + Demo Vaadin application with Playwright tests for k6 recording + + + + + + org.springframework.boot + spring-boot-dependencies + 4.0.4 + pom + import + + + + + + + com.vaadin + + vaadin + + + com.vaadin + vaadin-spring-boot-starter + + + org.parttio + line-awesome + 2.1.0 + + + com.vaadin + loadtest-helper + ${project.version} + test + + + + 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.microsoft.playwright + playwright + 1.50.0 + test + + + + + spring-boot:run + + + org.springframework.boot + spring-boot-maven-plugin + 4.0.1 + + + + repackage + + + + + + com.vaadin + flow-maven-plugin + ${flow.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 + flow-maven-plugin + ${flow.version} + + + + build-frontend + + compile + + + + true + + + + + + + diff --git a/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/Application.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/Application.java new file mode 100644 index 000000000..cc759c136 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/Application.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.laboratory; + +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; + +import com.vaadin.flow.component.dependency.StyleSheet; +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.theme.lumo.Lumo; +import com.vaadin.laboratory.data.SamplePersonRepository; + +/** + * 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-playwright/src/main/java/com/vaadin/laboratory/data/AbstractEntity.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/AbstractEntity.java new file mode 100644 index 000000000..2a0d24326 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/AbstractEntity.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +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-playwright/src/main/java/com/vaadin/laboratory/data/SamplePerson.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/SamplePerson.java new file mode 100644 index 000000000..fc4dd11ce --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/SamplePerson.java @@ -0,0 +1,101 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +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-playwright/src/main/java/com/vaadin/laboratory/data/SamplePersonRepository.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/SamplePersonRepository.java new file mode 100644 index 000000000..260407743 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/data/SamplePersonRepository.java @@ -0,0 +1,18 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +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-playwright/src/main/java/com/vaadin/laboratory/data/package-info.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/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-playwright/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-playwright/src/main/java/com/vaadin/laboratory/services/SamplePersonService.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/services/SamplePersonService.java new file mode 100644 index 000000000..c8312af61 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/services/SamplePersonService.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.laboratory.services; + +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; + +import com.vaadin.laboratory.data.SamplePerson; +import com.vaadin.laboratory.data.SamplePersonRepository; + +@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-playwright/src/main/java/com/vaadin/laboratory/services/package-info.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/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-playwright/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-playwright/src/main/java/com/vaadin/laboratory/views/MainLayout.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/MainLayout.java new file mode 100644 index 000000000..3f25396f4 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/MainLayout.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.laboratory.views; + +import java.util.List; +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; + +/** + * 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-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleFactory.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleFactory.java new file mode 100644 index 000000000..cca2a2319 --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleFactory.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.laboratory.views.crudexample; + +import org.springframework.stereotype.Component; + +import com.vaadin.flow.function.SerializableRunnable; +import com.vaadin.laboratory.services.SamplePersonService; + +@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-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleView.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleView.java new file mode 100644 index 000000000..4d197fcde --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/CrudExampleView.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.laboratory.views.crudexample; + +import java.util.Optional; + +import org.vaadin.lineawesome.LineAwesomeIconUrl; + +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.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.Menu; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.laboratory.data.SamplePerson; +import com.vaadin.laboratory.services.SamplePersonService; + +@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-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/PersonForm.java b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/PersonForm.java new file mode 100644 index 000000000..a10e61d6f --- /dev/null +++ b/vaadin-testbench-loadtest/load-tests/demo-web-app-playwright/src/main/java/com/vaadin/laboratory/views/crudexample/PersonForm.java @@ -0,0 +1,156 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.laboratory.views.crudexample; + +import org.springframework.orm.ObjectOptimisticLockingFailureException; + +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 com.vaadin.laboratory.data.SamplePerson; +import com.vaadin.laboratory.services.SamplePersonService; + +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