From a61392e7df6e6756e6405c155a55e16a84055072 Mon Sep 17 00:00:00 2001 From: caalador Date: Thu, 24 Oct 2024 14:32:32 +0300 Subject: [PATCH 1/6] test: Add react mixed test module (#6865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add react mixed test module Add a mixed React Hilla Flow test module to test interoperability of the 2 features Fixes #4972 * update to new 24.6 * react in name and description * fix application configuration packages. * Use abstractPlatformTest for test base * Remove log, fix test * Flow to hilla navigation. * fix menu find * Add some navigation tests. * Update with navigation tests. * remove name fusion and add react instead * Add Dashboard component * remove index to not get issues with flow route ´flow´ * module only behind profile --------- Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> --- pom.xml | 6 + .../pom-dev-mode.xml | 319 ++++++++++++++++++ vaadin-platform-react-hybrid-test/pom.xml | 293 ++++++++++++++++ .../src/main/frontend/index.html | 23 ++ .../frontend/themes/react-test/styles.css | 0 .../frontend/themes/react-test/theme.json | 1 + .../src/main/frontend/utils/css-utils.ts | 23 ++ .../src/main/frontend/views/@index.tsx | 25 ++ .../main/frontend/views/flow/hello-hilla.tsx | 25 ++ .../src/main/frontend/views/hilla/@index.tsx | 19 ++ .../src/main/frontend/views/hilla/@layout.tsx | 55 +++ .../main/frontend/views/hilla/components.tsx | 316 +++++++++++++++++ .../main/frontend/views/hilla/hello-react.tsx | 24 ++ .../platform/react/test/Application.java | 37 ++ .../react/test/views/FlowHillaView.java | 32 ++ .../platform/react/test/views/FlowLayout.java | 77 +++++ .../react/test/views/FlowMainView.java | 27 ++ .../react/test/views/HelloWorldView.java | 32 ++ .../META-INF/resources/icons/icon.png | Bin 0 -> 15994 bytes .../META-INF/resources/images/logo.png | Bin 0 -> 16070 bytes .../src/main/resources/application.properties | 8 + .../src/main/resources/banner.txt | 5 + .../react/test/AbstractPlatformTest.java | 131 +++++++ .../platform/react/test/ComponentsIT.java | 77 +++++ .../platform/react/test/FlowMainLayoutIT.java | 124 +++++++ .../react/test/HillaMainLayoutIT.java | 97 ++++++ 26 files changed, 1776 insertions(+) create mode 100644 vaadin-platform-react-hybrid-test/pom-dev-mode.xml create mode 100644 vaadin-platform-react-hybrid-test/pom.xml create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/index.html create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/themes/react-test/styles.css create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/themes/react-test/theme.json create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/utils/css-utils.ts create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/views/@index.tsx create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/views/flow/hello-hilla.tsx create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/@index.tsx create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/@layout.tsx create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/components.tsx create mode 100644 vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/hello-react.tsx create mode 100644 vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/Application.java create mode 100644 vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowHillaView.java create mode 100644 vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowLayout.java create mode 100644 vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowMainView.java create mode 100644 vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/HelloWorldView.java create mode 100644 vaadin-platform-react-hybrid-test/src/main/resources/META-INF/resources/icons/icon.png create mode 100644 vaadin-platform-react-hybrid-test/src/main/resources/META-INF/resources/images/logo.png create mode 100644 vaadin-platform-react-hybrid-test/src/main/resources/application.properties create mode 100644 vaadin-platform-react-hybrid-test/src/main/resources/banner.txt create mode 100644 vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/AbstractPlatformTest.java create mode 100644 vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/ComponentsIT.java create mode 100644 vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/FlowMainLayoutIT.java create mode 100644 vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/HillaMainLayoutIT.java diff --git a/pom.xml b/pom.xml index fd9f8decf..1d306b121 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,12 @@ vaadin-platform-hybrid-test + + react-hybrid + + vaadin-platform-react-hybrid-test + + https://github.com/vaadin/platform diff --git a/vaadin-platform-react-hybrid-test/pom-dev-mode.xml b/vaadin-platform-react-hybrid-test/pom-dev-mode.xml new file mode 100644 index 000000000..7749291a3 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/pom-dev-mode.xml @@ -0,0 +1,319 @@ + + 4.0.0 + + com.vaadin + vaadin-platform-parent + 24.6-SNAPSHOT + + vaadin-platform-react-hybrid-test + jar + Vaadin Platform React Hybrid Tests (Dev Mode) + Vaadin Platform React Hybrid Tests (Dev Mode) + https://vaadin.com + + 17 + 17 + UTF-8 + UTF-8 + false + true + + + + + com.vaadin + vaadin-bom + ${project.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + Vaadin Directory + https://maven.vaadin.com/vaadin-addons + + false + + + + + + + + central + https://repo.maven.apache.org/maven2 + + false + + + + vaadin-prereleases + https://maven.vaadin.com/vaadin-prereleases + + + vaadin-addons + https://maven.vaadin.com/vaadin-addons + + + + + + com.vaadin + vaadin + + + com.vaadin + vaadin-spring + + + org.springframework.boot + spring-boot-starter-web + + + ch.qos.logback + logback-classic + + + + + org.vaadin.artur + a-vaadin-helper + 1.5.0 + + + org.vaadin.artur.exampledata + exampledata + 3.0.0 + + + + org.springframework.boot + spring-boot-devtools + true + + + + com.vaadin + vaadin-testbench + test + ${project.version} + + + junit + junit + 4.13.1 + test + + + + org.slf4j + slf4j-simple + + + + + io.github.bonigarcia + webdrivermanager + 4.4.3 + test + + + + + + maven-clean-plugin + 3.1.0 + + + auto-clean + initialize + + clean + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + 500 + 240 + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + parse-version + + parse-version + + + + + + maven-failsafe-plugin + 2.20 + + + + javax.xml.bind + jaxb-api + 2.2.11 + + + com.sun.xml.bind + jaxb-core + 2.2.11 + + + com.sun.xml.bind + jaxb-impl + 2.2.11 + + + javax.activation + activation + 1.1.1 + + + + + + integration-test + verify + + + + + false + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + read-local-properties-if-present + initialize + + read-project-properties + + + + ../local.properties + + true + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + + true + + + + com.vaadin + vaadin-maven-plugin + ${project.version} + + + + prepare-frontend + + + + + + maven-antrun-plugin + 3.0.0 + + + compile + + ${ce.license} + + + run + + + + + + + + + + ITs + + + !skipTests + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + repackage + + + + start-spring-boot + pre-integration-test + + start + + + + stop-spring-boot + post-integration-test + + stop + + + + + com.vaadin.platform.react.test.Application + + + + + + + diff --git a/vaadin-platform-react-hybrid-test/pom.xml b/vaadin-platform-react-hybrid-test/pom.xml new file mode 100644 index 000000000..e633faa7f --- /dev/null +++ b/vaadin-platform-react-hybrid-test/pom.xml @@ -0,0 +1,293 @@ + + 4.0.0 + + com.vaadin + vaadin-platform-parent + 24.6-SNAPSHOT + + vaadin-platform-react-hybrid-test-prod + war + Vaadin Platform React Hybrid (flow and hilla views) Tests (Production Mode) + Vaadin Platform React Hybrid (flow and hilla views) Tests (Production Mode) + https://vaadin.com + + 17 + 17 + UTF-8 + UTF-8 + false + true + + + + + com.vaadin + vaadin-bom + ${project.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + Vaadin Directory + https://maven.vaadin.com/vaadin-addons + + false + + + + + + + + central + https://repo.maven.apache.org/maven2 + + false + + + + vaadin-prereleases + https://maven.vaadin.com/vaadin-prereleases + + + vaadin-addons + https://maven.vaadin.com/vaadin-addons + + + + + + com.vaadin + vaadin + + + com.vaadin + vaadin-spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + ch.qos.logback + logback-classic + + + + + + org.springframework.boot + spring-boot-devtools + true + + + + com.vaadin + vaadin-testbench + test + ${project.version} + + + junit + junit + 4.13.1 + test + + + + org.slf4j + slf4j-simple + + + + io.github.bonigarcia + webdrivermanager + 4.4.3 + test + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + parse-version + + parse-version + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + read-local-properties-if-present + initialize + + read-project-properties + + + + ../local.properties + + true + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + + true + + + + com.vaadin + vaadin-maven-plugin + + + + prepare-frontend + + + + + + + + + + ITs + + + !skipTests + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + repackage + + + + start-spring-boot + pre-integration-test + + start + + + + stop-spring-boot + post-integration-test + + stop + + + + + com.vaadin.platform.react.test.Application + + + + maven-failsafe-plugin + 2.20 + + + + javax.xml.bind + jaxb-api + 2.2.11 + + + com.sun.xml.bind + jaxb-core + 2.2.11 + + + com.sun.xml.bind + jaxb-impl + 2.2.11 + + + javax.activation + activation + 1.1.1 + + + + + + integration-test + verify + + + + + false + + + + + + + production + + + vaadin.productionMode + + + + + + com.vaadin + vaadin-maven-plugin + ${project.version} + + + + build-frontend + + + + + + + + + diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/index.html b/vaadin-platform-react-hybrid-test/src/main/frontend/index.html new file mode 100644 index 000000000..d36e59347 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/themes/react-test/styles.css b/vaadin-platform-react-hybrid-test/src/main/frontend/themes/react-test/styles.css new file mode 100644 index 000000000..e69de29bb diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/themes/react-test/theme.json b/vaadin-platform-react-hybrid-test/src/main/frontend/themes/react-test/theme.json new file mode 100644 index 000000000..653b73c07 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/frontend/themes/react-test/theme.json @@ -0,0 +1 @@ +{"lumoImports":["typography","color","badge"]} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/utils/css-utils.ts b/vaadin-platform-react-hybrid-test/src/main/frontend/utils/css-utils.ts new file mode 100644 index 000000000..74bb3479f --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/frontend/utils/css-utils.ts @@ -0,0 +1,23 @@ +import { DomModule } from "@polymer/polymer/lib/elements/dom-module"; +import { stylesFromTemplate } from "@polymer/polymer/lib/utils/style-gather"; +import { CSSResult, unsafeCSS } from "lit"; + +/** + * Utility function for importing style modules. This is a temporary + * solution until there is a standard solution available + * @see https://github.com/vaadin/vaadin-themable-mixin/issues/73 + * + * @param id the style module to import + */ +export const CSSModule = (id: string): CSSResult => { + const template: HTMLTemplateElement | null = DomModule.import( + id, + "template" + ) as HTMLTemplateElement; + const cssText = + template && + stylesFromTemplate(template, "") + .map((style: HTMLStyleElement) => style.textContent) + .join(" "); + return unsafeCSS(cssText); +}; diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/views/@index.tsx b/vaadin-platform-react-hybrid-test/src/main/frontend/views/@index.tsx new file mode 100644 index 000000000..7e1779061 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/frontend/views/@index.tsx @@ -0,0 +1,25 @@ +import {Button, VerticalLayout} from "@vaadin/react-components"; +import type { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import {useNavigate} from "react-router-dom"; + +export const config: ViewConfig = { + menu: { + title: "root", + }, + flowLayout: false +}; + +/** + * Hilla view that is available publicly. + */ +export default function Public() { + const navigate = useNavigate(); + + return ( + +

This is the Hill index page.

+ + +
+ ); +} diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/views/flow/hello-hilla.tsx b/vaadin-platform-react-hybrid-test/src/main/frontend/views/flow/hello-hilla.tsx new file mode 100644 index 000000000..0910f8214 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/frontend/views/flow/hello-hilla.tsx @@ -0,0 +1,25 @@ +import { Button, TextField, VerticalLayout } from "@vaadin/react-components"; +import type { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import { useState } from "react"; +import { Notification } from '@vaadin/react-components/Notification.js'; + +export const config: ViewConfig = { + menu: { + title: "Hello React in Flow Layout", + }, + title: "Hilla in Flow" +}; + +export default function HelloHilla() { + const [name, setName] = useState(""); + + return ( + + setName(e.detail.value)} /> + + + ) + ; +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/@index.tsx b/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/@index.tsx new file mode 100644 index 000000000..bf1730603 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/@index.tsx @@ -0,0 +1,19 @@ +import type {ViewConfig} from "@vaadin/hilla-file-router/types.js"; +import { NavLink } from "react-router-dom"; + +export const config: ViewConfig = { + menu: { + exclude: true, + title: "ERROR!", + }, +}; + +export default function Hilla() { + return ( +
+ +
"Hilla root view for menu!"
+ To hello react +
+ ); +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/@layout.tsx b/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/@layout.tsx new file mode 100644 index 000000000..121246ee0 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/@layout.tsx @@ -0,0 +1,55 @@ +import { + AppLayout, + DrawerToggle, + Icon, + SideNav, + SideNavItem +} from "@vaadin/react-components"; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +import { createMenuItems, useViewConfig } from '@vaadin/hilla-file-router/runtime.js'; +import { effect, Signal, signal } from "@vaadin/hilla-react-signals"; + +const vaadin = window.Vaadin as { + documentTitleSignal: Signal; +}; +vaadin.documentTitleSignal = signal(""); +effect(() => { document.title = vaadin.documentTitleSignal.value; }); + +export default function Layout() { + const navigate = useNavigate(); + const location = useLocation(); + vaadin.documentTitleSignal.value = useViewConfig()?.title ?? ''; + + + return ( + +
+
+

Hybrid Example With Stateful Auth

+ navigate(path!)} + location={location}> + { + createMenuItems().filter(({to}) => { + return to.startsWith("hilla") || to.startsWith("/hilla"); + }).map(({ to, icon, title }) => ( + + {icon && } + {title} + + )) + } + +
+ +
+ + +

+ {vaadin.documentTitleSignal} +

+ + +
+ ); +} diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/components.tsx b/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/components.tsx new file mode 100644 index 000000000..41c05280c --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/components.tsx @@ -0,0 +1,316 @@ +import { + Accordion, + AccordionHeading, + AccordionPanel, + Avatar, + AvatarGroup, + Button, + Checkbox, + CheckboxGroup, + ComboBox, + ConfirmDialog, + ContextMenu, + DatePicker, + DateTimePicker, + Details, + DetailsSummary, + Dialog, + DrawerToggle, + EmailField, + FormLayout, + Grid, + GridColumn, + GridSelectionColumn, + GridSortColumn, + GridSorter, + GridTreeToggle, + HorizontalLayout, + Icon, + Iconset, + IntegerField, + Item, + ListBox, + LoginForm, + LoginOverlay, + MenuBar, + Message, + MessageInput, + MessageList, + MultiSelectComboBox, + Notification, + NumberField, + PasswordField, + Popover, + ProgressBar, RadioButton, RadioGroup, + Scroller, + Select, SideNav, SideNavItem, SplitLayout, Tab, + Tabs, TabSheet, TabSheetTab, + TextArea, + TextField, + TimePicker, + Tooltip, + Upload, + VerticalLayout, + VirtualList, + VirtualListItemModel +} from "@vaadin/react-components"; +import type { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import { + Board, + BoardRow, + Chart, + ChartSeries, + CookieConsent, + Crud, + CrudEditColumn, Dashboard, + GridPro, + GridProEditColumn, + RichTextEditor +} from "@vaadin/react-components-pro"; + +export const config: ViewConfig = { + menu: { + title: "React Components", + } +}; + +export default function Components() { + const openLoginOverlay = () => { + // @ts-ignore + document.getElementsByTagName("vaadin-login-overlay").item(0).opened = true; + } + + return ( + + + + summary +
accordion content
+
+
+ + + + + +
top aA
+
top B
+
top C
+
+ +
mid
+
+ +
low A
+ +
low B / A
+
low B / B
+
low B / C
+
low B / D
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Context Menu

+
+ + + + + + + + + + + + + + +
+ Summary +
Details
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

H1

H2

H3

H4

H5
+
H6
+
+ +
Header
+
DIV
+
Footer
+
+
+ + + Select an Item + Item one + Item two +
+ Item three + Item four +
+ + + + + + + + + + + + + + + + + + + + + + Main menu + Nav Item 1 + + Nav Item 2 + Nav Item 2 - + 1 + Nav Item 2 - + 2 + + + + +
+
+
+ + + +
Panel 1
+
+ +
Panel 2
+
+ +
Panel 3
+
+
+ + + Tab 1 + Tab 2 + Tab 3 + + + + + + + + + +
+ Notice
+ Content +
+
+ + + + +
+ ); +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/hello-react.tsx b/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/hello-react.tsx new file mode 100644 index 000000000..aabb8f682 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/frontend/views/hilla/hello-react.tsx @@ -0,0 +1,24 @@ +import { Button, TextField, VerticalLayout } from "@vaadin/react-components"; +import type { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import { useState } from "react"; +import { Notification } from '@vaadin/react-components/Notification.js'; + +export const config: ViewConfig = { + menu: { + title: "Hello World React", + } +}; + +export default function HelloReact() { + const [name, setName] = useState(""); + + return ( + + setName(e.detail.value)} /> + + + ); +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/Application.java b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/Application.java new file mode 100644 index 000000000..1f6c552be --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/Application.java @@ -0,0 +1,37 @@ +package com.vaadin.platform.react.test; + +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.server.PWA; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; + +import com.vaadin.flow.server.auth.DefaultMenuAccessControl; +import com.vaadin.flow.server.auth.MenuAccessControl; +import com.vaadin.flow.theme.Theme; + +/** + * The entry point of the Spring Boot application. + * + * Use the * and some desktop browsers. + * + */ +@SpringBootApplication +@Theme(value = "react-test") +@PWA(name = "react-test", shortName = "react-test", offlineResources = {"images/logo.png"}) +public class Application extends SpringBootServletInitializer implements AppShellConfigurator { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public MenuAccessControl customMenuAccessControl() { + var menuAccessControl = new DefaultMenuAccessControl(); + menuAccessControl.setPopulateClientSideMenu( + MenuAccessControl.PopulateClientMenu.ALWAYS); + return menuAccessControl; + } +} diff --git a/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowHillaView.java b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowHillaView.java new file mode 100644 index 000000000..605e9e314 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowHillaView.java @@ -0,0 +1,32 @@ +package com.vaadin.platform.react.test.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Menu; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@Route("hilla/flow") +@PageTitle("Hello Hilla") +@Menu(title = "Flow in hilla") +public class FlowHillaView extends HorizontalLayout { + + private TextField name; + private Button sayHello; + + public FlowHillaView() { + setId("flow-hilla"); + setPadding(true); + setSpacing(true); + name = new TextField("Your name for Flow"); + sayHello = new Button("Say hello"); + add(name, sayHello); + setVerticalComponentAlignment(Alignment.END, name, sayHello); + sayHello.addClickListener(e -> { + Notification.show("Hello " + name.getValue()); + }); + } + +} diff --git a/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowLayout.java b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowLayout.java new file mode 100644 index 000000000..153e0e706 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowLayout.java @@ -0,0 +1,77 @@ +package com.vaadin.platform.react.test.views; + +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.orderedlayout.Scroller; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.Layout; +import com.vaadin.flow.router.RoutePrefix; +import com.vaadin.flow.server.menu.MenuConfiguration; +import com.vaadin.flow.theme.lumo.LumoUtility; + +@Layout("/flow") +@RoutePrefix("flow") +public class FlowLayout extends AppLayout { + + private H1 viewTitle; + + public FlowLayout() { + 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("Flow menu"); + 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(); + + MenuConfiguration.getMenuEntries().forEach(menuEntry -> { + if(menuEntry.path().startsWith("flow") || menuEntry.path().startsWith("/flow")) { + nav.addItem( + new SideNavItem(menuEntry.title(), menuEntry.path())); + } + }); + + return nav; + } + + private Footer createFooter() { + Footer layout = new Footer(); + + return layout; + } + + @Override + protected void afterNavigation() { + super.afterNavigation(); + viewTitle.setText(getCurrentPageTitle()); + } + + private String getCurrentPageTitle() { + return MenuConfiguration.getPageHeader(getContent()).orElse(""); + } +} diff --git a/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowMainView.java b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowMainView.java new file mode 100644 index 000000000..082f003fd --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/FlowMainView.java @@ -0,0 +1,27 @@ +package com.vaadin.platform.react.test.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@Route("flow") +public class FlowMainView extends VerticalLayout { + public FlowMainView() { + Span span = new Span("Flow root view for menu!"); + span.setId("flow-main"); + add(span); + + RouterLink flow = new RouterLink("Flow with RouterLink", + HelloWorldView.class); + flow.setId("flow-link"); + + Button flowButton = new Button("Flow with Button", + e -> e.getSource().getUI().get() + .navigate(HelloWorldView.class)); + flowButton.setId("flow-button"); + + add(flow, flowButton); + } +} diff --git a/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/HelloWorldView.java b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/HelloWorldView.java new file mode 100644 index 000000000..89ffbacf2 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/java/com/vaadin/platform/react/test/views/HelloWorldView.java @@ -0,0 +1,32 @@ +package com.vaadin.platform.react.test.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Menu; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@Route("flow/hello-world") +@PageTitle("Hello World") +@Menu(title = "Flow Hello") +public class HelloWorldView extends HorizontalLayout { + + private TextField name; + private Button sayHello; + + public HelloWorldView() { + setId("flow-hello"); + setPadding(true); + setSpacing(true); + name = new TextField("Your name"); + sayHello = new Button("Say hello"); + add(name, sayHello); + setVerticalComponentAlignment(Alignment.END, name, sayHello); + sayHello.addClickListener(e -> { + Notification.show("Hello " + name.getValue()); + }); + } + +} diff --git a/vaadin-platform-react-hybrid-test/src/main/resources/META-INF/resources/icons/icon.png b/vaadin-platform-react-hybrid-test/src/main/resources/META-INF/resources/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6fde5c10d44ec14b621ef75e6b283f564c017f38 GIT binary patch literal 15994 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 getMenuElement(String label) { + List items = $(SideNavElement.class).first() + .getItems(); + return items.stream().filter(item -> item.getLabel().equals(label)) + .findFirst(); + } + + /** + * Wait for element else fail with message. + * + * @param message failure message + * @param by By for locating element + */ + protected void waitForElement(String message, By by) { + try { + waitUntil(ExpectedConditions.presenceOfElementLocated(by)); + } catch (TimeoutException te) { + Assert.fail(message); + } + } +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/ComponentsIT.java b/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/ComponentsIT.java new file mode 100644 index 000000000..1ab2e15b5 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/ComponentsIT.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2023 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.platform.react.test; + +import static org.junit.Assert.assertNotNull; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Level; +import java.util.stream.Collectors; + +import org.junit.Test; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.logging.LogEntry; +import org.openqa.selenium.logging.LogType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.button.testbench.ButtonElement; +import com.vaadin.flow.component.login.testbench.LoginOverlayElement; +import com.vaadin.flow.component.notification.testbench.NotificationElement; +import com.vaadin.flow.component.orderedlayout.testbench.VerticalLayoutElement; + +public class ComponentsIT extends AbstractPlatformTest { + + @Test + public void loadComponents() throws Exception { + // There should not be component errors in console + checkLogsForErrors(); + // Notification opens when loaded + $(NotificationElement.class).waitForFirst(); + + // Do something with the view + VerticalLayoutElement mainLayout = $(VerticalLayoutElement.class).id( + "components"); + mainLayout.$(ButtonElement.class).id("open-overlay").click(); + + assertNotNull($(LoginOverlayElement.class).waitForFirst()); + } + + private void checkLogsForErrors() { + getLogEntries(Level.WARNING).forEach(logEntry -> { + if ((Objects.equals(logEntry.getLevel(), Level.SEVERE) || logEntry.getMessage().contains("404"))) { + throw new AssertionError(String.format( + "Received error message in browser log console right after opening the page, message: %s", logEntry)); + } else { + getLogger().warn("This message in browser log console may be a potential error: '{}'", logEntry); + } + }); + } + + private List getLogEntries(Level level) { + getCommandExecutor().waitForVaadin(); + return driver.manage().logs().get(LogType.BROWSER).getAll().stream() + .filter(logEntry -> logEntry.getLevel().intValue() >= level.intValue()) + // exclude the favicon error + .filter(logEntry -> !logEntry.getMessage().contains("favicon.ico")).collect(Collectors.toList()); + } + + @Override + protected String getTestPath() { + return "/hilla/components"; + } +} diff --git a/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/FlowMainLayoutIT.java b/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/FlowMainLayoutIT.java new file mode 100644 index 000000000..c7eec262f --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/FlowMainLayoutIT.java @@ -0,0 +1,124 @@ +package com.vaadin.platform.react.test; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.support.ui.ExpectedConditions; + +import com.vaadin.flow.component.button.testbench.ButtonElement; +import com.vaadin.flow.component.html.testbench.AnchorElement; + +public class FlowMainLayoutIT extends AbstractPlatformTest { + + @Test + public void hillaViewInFlowLayout() { + waitUntil(ExpectedConditions.presenceOfElementLocated( + By.id("flow-main"))); + + // Navigate to Flow view + getMenuElement("Hello React in Flow Layout").get().click(); + + waitUntil(ExpectedConditions.presenceOfElementLocated( + By.id("flow-hilla"))); + + // navigate away from Flow view + getMenuElement("Flow Hello").get().click(); + + waitForElement("Should have navigated to HelloWorld Flow", + By.id("flow-hello")); + } + + @Test + public void navigateWithRouterLink() { + waitForElement("Expected flow main view on load", + By.id("flow-main")); + + $(AnchorElement.class).id("flow-link").click(); + + waitForElement("Should have navigated to HelloWorld Flow", + By.id("flow-hello")); + + } + + @Test + public void navigateWithUINavigate() { + waitForElement("Expected flow main view on load", + By.id("flow-main")); + $(ButtonElement.class).id("flow-button").click(); + + waitForElement("Should have navigated to HelloWorld Flow", + By.id("flow-hello")); + } + + @Test + public void backNavigationTest() { + waitForElement("Expected flow main view on load", + By.id("flow-main")); + + // Navigate to Flow view + getMenuElement("Hello React in Flow Layout").get().click(); + + waitForElement("Expected hilla view", + By.id("flow-hilla")); + + getDriver().navigate().back(); + + waitForElement("Expected flow main view for back", + By.id("flow-main")); + + // navigate away from Flow view + getMenuElement("Flow Hello").get().click(); + + waitForElement("Expected flow view", + By.id("flow-hello")); + + getDriver().navigate().back(); + + waitForElement("Expected flow main view for back", + By.id("flow-main")); + } + + @Test + public void forwardNavigationTest() { + waitForElement("Expected flow main view on load", + By.id("flow-main")); + + // Navigate to Flow view + getMenuElement("Hello React in Flow Layout").get().click(); + + waitForElement("Expected hilla view", + By.id("flow-hilla")); + + // navigate away to Flow view + getMenuElement("Flow Hello").get().click(); + + waitForElement("Expected flow view", + By.id("flow-hello")); + + getDriver().navigate().back(); + + waitForElement("Expected hilla view after back", + By.id("flow-hilla")); + + getDriver().navigate().back(); + + waitForElement("Expected flow main view after second back", + By.id("flow-main")); + + getDriver().navigate().forward(); + + waitForElement("Expected hilla view for forward", + By.id("flow-hilla")); + + getDriver().navigate().forward(); + + waitForElement("Expected flow view for second forward", + By.id("flow-hello")); + } + + @Override + protected String getTestPath() { + return "/flow"; + } +} diff --git a/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/HillaMainLayoutIT.java b/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/HillaMainLayoutIT.java new file mode 100644 index 000000000..95fcf07f9 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/HillaMainLayoutIT.java @@ -0,0 +1,97 @@ +package com.vaadin.platform.react.test; + + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.support.ui.ExpectedConditions; + +import com.vaadin.flow.component.button.testbench.ButtonElement; +import com.vaadin.flow.component.orderedlayout.testbench.VerticalLayoutElement; + +public class HillaMainLayoutIT extends AbstractPlatformTest { + + @Test + public void flowViewInHillaLayout() { + Assert.assertNotNull(findElement(By.id("hilla"))); + + // Navigate to Flow view + getMenuElement("Flow in hilla").get().click(); + + waitUntil(ExpectedConditions.presenceOfElementLocated( + By.id("flow-hilla"))); + + // navigate away from Flow view + getMenuElement("React Components").get().click(); + + Assert.assertTrue("React components view should be shown", + $(ButtonElement.class).id("open-overlay").isDisplayed()); + } + + @Test + public void navigateUsingNavLink() { + findElement(By.id("toHello")).click(); + + Assert.assertTrue("Navigation with NavLink failed.", + $(VerticalLayoutElement.class).id("HelloReact").isDisplayed()); + } + + @Test + public void backNavigationTest() { + findElement(By.id("toHello")).click(); + + Assert.assertTrue("Navigation with NavLink failed.", + $(VerticalLayoutElement.class).id("HelloReact").isDisplayed()); + + getDriver().navigate().back(); + + Assert.assertTrue( + "Should have returned to initial page from Hilla view", + findElement(By.id("toHello")).isDisplayed()); + + // Navigate to Flow view + getMenuElement("Flow in hilla").get().click(); + + waitForElement("Expected flow view", By.id("flow-hilla")); + + getDriver().navigate().back(); + + Assert.assertTrue("Should have returned to initial page from Flow view", + findElement(By.id("toHello")).isDisplayed()); + } + + + @Test + public void forwardNavigationTest() { + findElement(By.id("toHello")).click(); + + Assert.assertTrue("Navigation with NavLink failed.", + $(VerticalLayoutElement.class).id("HelloReact").isDisplayed()); + + // Navigate to Flow view + getMenuElement("Flow in hilla").get().click(); + + waitForElement("Expected flow view", By.id("flow-hilla")); + + getDriver().navigate().back(); + getDriver().navigate().back(); + + Assert.assertTrue("Should have returned to initial page", + findElement(By.id("toHello")).isDisplayed()); + + getDriver().navigate().forward(); + + Assert.assertTrue("Expected hilla view after forward", + $(VerticalLayoutElement.class).id("HelloReact").isDisplayed()); + + getDriver().navigate().forward(); + + waitForElement("Expected flow view after second forward", + By.id("flow-hilla")); + } + + @Override + protected String getTestPath() { + return "/hilla"; + } +} From 0894b7c67c681b4b2b9af3ef5a54141e09f9ac51 Mon Sep 17 00:00:00 2001 From: caalador Date: Thu, 24 Oct 2024 15:03:06 +0300 Subject: [PATCH 2/6] test: Flow-Hilla with security module (#6899) * feat: Flow-Hilla with security module Add test module for Flow-Hilla mixed application with security configuration. part of #4972 * Add views with roles. Add tests for with and without login for both hilla and flow main layout * add security module * rename profile --------- Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> --- pom.xml | 6 + .../pom-dev-mode.xml | 333 ++++++++++++++++++ .../pom.xml | 313 ++++++++++++++++ .../src/main/frontend/auth.tsx | 9 + .../src/main/frontend/index.html | 23 ++ .../src/main/frontend/index.tsx | 17 + .../frontend/themes/react-test/styles.css | 0 .../frontend/themes/react-test/theme.json | 1 + .../src/main/frontend/views/@index.tsx | 25 ++ .../src/main/frontend/views/flow/@index.tsx | 17 + .../main/frontend/views/flow/hello-hilla.tsx | 23 ++ .../src/main/frontend/views/hilla/@index.tsx | 19 + .../src/main/frontend/views/hilla/@layout.tsx | 84 +++++ .../src/main/frontend/views/hilla/admin.tsx | 21 ++ .../main/frontend/views/hilla/hello-react.tsx | 19 + .../src/main/frontend/views/hilla/user.tsx | 21 ++ .../src/main/frontend/views/login.tsx | 58 +++ .../platform/react/test/Application.java | 37 ++ .../platform/react/test/security/Role.java | 6 + .../react/test/security/SecurityConfig.java | 56 +++ .../react/test/security/UserInfo.java | 32 ++ .../react/test/security/UserInfoService.java | 34 ++ .../react/test/views/AccessLayout.java | 16 + .../platform/react/test/views/AdminView.java | 19 + .../react/test/views/FlowHillaView.java | 32 ++ .../platform/react/test/views/FlowLayout.java | 100 ++++++ .../react/test/views/FlowMainView.java | 29 ++ .../react/test/views/LayoutSecuredView.java | 21 ++ .../platform/react/test/views/PublicView.java | 34 ++ .../platform/react/test/views/UserView.java | 19 + .../META-INF/resources/icons/icon.png | Bin 0 -> 15994 bytes .../META-INF/resources/images/logo.png | Bin 0 -> 16070 bytes .../src/main/resources/application.properties | 8 + .../src/main/resources/banner.txt | 5 + .../react/test/AbstractPlatformTest.java | 140 ++++++++ .../platform/react/test/FlowMainLayoutIT.java | 111 ++++++ .../react/test/HillaMainLayoutIT.java | 103 ++++++ 37 files changed, 1791 insertions(+) create mode 100644 vaadin-platform-react-hybrid-security-test/pom-dev-mode.xml create mode 100644 vaadin-platform-react-hybrid-security-test/pom.xml create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/auth.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/index.html create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/index.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/themes/react-test/styles.css create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/themes/react-test/theme.json create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/views/@index.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/views/flow/@index.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/views/flow/hello-hilla.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/@index.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/@layout.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/admin.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/hello-react.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/user.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/frontend/views/login.tsx create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/Application.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/Role.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/SecurityConfig.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/UserInfo.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/UserInfoService.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/AccessLayout.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/AdminView.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowHillaView.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowLayout.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowMainView.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/LayoutSecuredView.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/PublicView.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/UserView.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/resources/META-INF/resources/icons/icon.png create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/resources/META-INF/resources/images/logo.png create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/resources/application.properties create mode 100644 vaadin-platform-react-hybrid-security-test/src/main/resources/banner.txt create mode 100644 vaadin-platform-react-hybrid-security-test/src/test/java/com/vaadin/platform/react/test/AbstractPlatformTest.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/test/java/com/vaadin/platform/react/test/FlowMainLayoutIT.java create mode 100644 vaadin-platform-react-hybrid-security-test/src/test/java/com/vaadin/platform/react/test/HillaMainLayoutIT.java diff --git a/pom.xml b/pom.xml index 1d306b121..f44a8c399 100644 --- a/pom.xml +++ b/pom.xml @@ -101,6 +101,12 @@ vaadin-platform-react-hybrid-test + + hybrid-security + + vaadin-platform-react-hybrid-security-test + + https://github.com/vaadin/platform diff --git a/vaadin-platform-react-hybrid-security-test/pom-dev-mode.xml b/vaadin-platform-react-hybrid-security-test/pom-dev-mode.xml new file mode 100644 index 000000000..c4ba735dd --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/pom-dev-mode.xml @@ -0,0 +1,333 @@ + + 4.0.0 + + com.vaadin + vaadin-platform-parent + 24.6-SNAPSHOT + + vaadin-platform-react-hybrid-test + jar + Vaadin Platform React Hybrid Tests (Dev Mode) + Vaadin Platform React Hybrid Tests (Dev Mode) + https://vaadin.com + + 17 + 17 + UTF-8 + UTF-8 + false + true + + + + + com.vaadin + vaadin-bom + ${project.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + Vaadin Directory + https://maven.vaadin.com/vaadin-addons + + false + + + + + + + + central + https://repo.maven.apache.org/maven2 + + false + + + + vaadin-prereleases + https://maven.vaadin.com/vaadin-prereleases + + + vaadin-addons + https://maven.vaadin.com/vaadin-addons + + + + + + com.vaadin + vaadin + + + com.vaadin + vaadin-spring + + + org.springframework.boot + spring-boot-starter-web + + + ch.qos.logback + logback-classic + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.vaadin.artur + a-vaadin-helper + 1.5.0 + + + org.vaadin.artur.exampledata + exampledata + 3.0.0 + + + + org.springframework.boot + spring-boot-devtools + true + + + + com.vaadin + vaadin-testbench + test + ${project.version} + + + junit + junit + 4.13.1 + test + + + + org.slf4j + slf4j-simple + + + + + io.github.bonigarcia + webdrivermanager + 4.4.3 + test + + + org.jsoup + jsoup + + + + + + + + maven-clean-plugin + 3.1.0 + + + auto-clean + initialize + + clean + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + 500 + 240 + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + parse-version + + parse-version + + + + + + maven-failsafe-plugin + 2.20 + + + + javax.xml.bind + jaxb-api + 2.2.11 + + + com.sun.xml.bind + jaxb-core + 2.2.11 + + + com.sun.xml.bind + jaxb-impl + 2.2.11 + + + javax.activation + activation + 1.1.1 + + + + + + integration-test + verify + + + + + false + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + read-local-properties-if-present + initialize + + read-project-properties + + + + ../local.properties + + true + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + + true + + + + com.vaadin + vaadin-maven-plugin + ${project.version} + + + + prepare-frontend + + + + + + maven-antrun-plugin + 3.0.0 + + + compile + + ${ce.license} + + + run + + + + + + + + + + ITs + + + !skipTests + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + repackage + + + + start-spring-boot + pre-integration-test + + start + + + + stop-spring-boot + post-integration-test + + stop + + + + + com.vaadin.platform.react.test.Application + + + + + + + diff --git a/vaadin-platform-react-hybrid-security-test/pom.xml b/vaadin-platform-react-hybrid-security-test/pom.xml new file mode 100644 index 000000000..796b6393f --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/pom.xml @@ -0,0 +1,313 @@ + + 4.0.0 + + com.vaadin + vaadin-platform-parent + 24.6-SNAPSHOT + + + + + + + + vaadin-platform-react-hybrid-test-prod + war + Vaadin Platform React Hybrid (flow and hilla views) Tests (Production Mode) + Vaadin Platform React Hybrid (flow and hilla views) Tests (Production Mode) + https://vaadin.com + + 17 + 17 + UTF-8 + UTF-8 + false + true + + + + + com.vaadin + vaadin-bom + 24.6-SNAPSHOT + pom + import + + + org.springframework.boot + spring-boot-dependencies + 3.3.4 + pom + import + + + + + + + Vaadin Directory + https://maven.vaadin.com/vaadin-addons + + false + + + + + + + + central + https://repo.maven.apache.org/maven2 + + false + + + + vaadin-prereleases + https://maven.vaadin.com/vaadin-prereleases + + + vaadin-addons + https://maven.vaadin.com/vaadin-addons + + + + + + com.vaadin + vaadin + + + com.vaadin + vaadin-spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + ch.qos.logback + logback-classic + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-devtools + true + + + + com.vaadin + vaadin-testbench + test + 24.6-SNAPSHOT + + + junit + junit + 4.13.1 + test + + + + org.slf4j + slf4j-simple + + + + io.github.bonigarcia + webdrivermanager + 4.4.3 + test + + + org.jsoup + jsoup + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + parse-version + + parse-version + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + read-local-properties-if-present + initialize + + read-project-properties + + + + ../local.properties + + true + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + + true + + + + com.vaadin + vaadin-maven-plugin + + + + prepare-frontend + + + + + + + + + + ITs + + + !skipTests + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + repackage + + + + start-spring-boot + pre-integration-test + + start + + + + stop-spring-boot + post-integration-test + + stop + + + + + com.vaadin.platform.react.test.Application + + + + maven-failsafe-plugin + 2.20 + + + + javax.xml.bind + jaxb-api + 2.2.11 + + + com.sun.xml.bind + jaxb-core + 2.2.11 + + + com.sun.xml.bind + jaxb-impl + 2.2.11 + + + javax.activation + activation + 1.1.1 + + + + + + integration-test + verify + + + + + false + + + + + + + production + + + vaadin.productionMode + + + + + + com.vaadin + vaadin-maven-plugin + ${project.version} + + + + build-frontend + + + + + + + + + diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/auth.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/auth.tsx new file mode 100644 index 000000000..ff60eb19c --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/auth.tsx @@ -0,0 +1,9 @@ +import { UserInfoService } from 'Frontend/generated/endpoints'; +import {configureAuth} from "@vaadin/hilla-react-auth"; + +const auth = configureAuth(UserInfoService.getUserInfo, { + getRoles: (userInfo) => userInfo.authorities, +}); + +export const useAuth = auth.useAuth; +export const AuthProvider = auth.AuthProvider; \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/index.html b/vaadin-platform-react-hybrid-security-test/src/main/frontend/index.html new file mode 100644 index 000000000..d36e59347 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/index.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/index.tsx new file mode 100644 index 000000000..22005f3b6 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/index.tsx @@ -0,0 +1,17 @@ +/****************************************************************************** + * Copied from generated file to wrap in AuthProvider + ******************************************************************************/ + +import { createElement } from 'react'; +import { createRoot } from 'react-dom/client'; +import { RouterProvider } from 'react-router-dom'; +import { router } from 'Frontend/generated/routes'; +import { AuthProvider } from './auth'; + +function App() { + return + + ; +} + +createRoot(document.getElementById('outlet')!).render(createElement(App)); diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/themes/react-test/styles.css b/vaadin-platform-react-hybrid-security-test/src/main/frontend/themes/react-test/styles.css new file mode 100644 index 000000000..e69de29bb diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/themes/react-test/theme.json b/vaadin-platform-react-hybrid-security-test/src/main/frontend/themes/react-test/theme.json new file mode 100644 index 000000000..653b73c07 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/themes/react-test/theme.json @@ -0,0 +1 @@ +{"lumoImports":["typography","color","badge"]} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/@index.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/@index.tsx new file mode 100644 index 000000000..cdc829cb6 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/@index.tsx @@ -0,0 +1,25 @@ +import {Button, VerticalLayout} from "@vaadin/react-components"; +import type { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import {useNavigate} from "react-router-dom"; + +export const config: ViewConfig = { + menu: { + title: "root", + }, + flowLayout: false +}; + +/** + * Hilla view that is available publicly. + */ +export default function Public() { + const navigate = useNavigate(); + + return ( + +

This is the Hill index page.

+ + +
+ ); +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/flow/@index.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/flow/@index.tsx new file mode 100644 index 000000000..eee7dc1d3 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/flow/@index.tsx @@ -0,0 +1,17 @@ +import type {ViewConfig} from "@vaadin/hilla-file-router/types.js"; + +export const config: ViewConfig = { + menu: { + exclude: true, + title: "ERROR!", + }, +}; + +export default function HelloHilla() { + return ( +
+
Place holder! This will render flow route as a flow layout will be requested!
+
Also tests Hilla menu exclude feature!
+
+ ); +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/flow/hello-hilla.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/flow/hello-hilla.tsx new file mode 100644 index 000000000..3855963c6 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/flow/hello-hilla.tsx @@ -0,0 +1,23 @@ +import { VerticalLayout } from "@vaadin/react-components"; +import type { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import { useState } from "react"; + +export const config: ViewConfig = { + menu: { + title: "Hello React in Flow Layout", + }, + title: "Hilla in Flow", + loginRequired: true, + rolesAllowed: [ "ROLE_USER", "ROLE_ADMIN" ] +}; + +export default function HelloHilla() { + const [name, setName] = useState(""); + + return ( + + Hilla in Flow Layout with USER/ADMIN login required! + + ) + ; +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/@index.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/@index.tsx new file mode 100644 index 000000000..0d6d73a28 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/@index.tsx @@ -0,0 +1,19 @@ +import type {ViewConfig} from "@vaadin/hilla-file-router/types.js"; +import { NavLink } from "react-router-dom"; + +export const config: ViewConfig = { + menu: { + exclude: true, + title: "ERROR!", + }, +}; + +export default function Hilla() { + return ( +
+ +
"Hilla root view for menu!"
+ To hello react +
+ ); +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/@layout.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/@layout.tsx new file mode 100644 index 000000000..668f9023f --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/@layout.tsx @@ -0,0 +1,84 @@ +import { + AppLayout, Avatar, Button, + DrawerToggle, + Icon, + SideNav, + SideNavItem +} from "@vaadin/react-components"; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +import { createMenuItems, useViewConfig } from '@vaadin/hilla-file-router/runtime.js'; +import { effect, Signal, signal } from "@vaadin/hilla-react-signals"; +import { useAuth } from "../../auth"; + +const vaadin = window.Vaadin as { + documentTitleSignal: Signal; +}; +vaadin.documentTitleSignal = signal(""); +effect(() => { document.title = vaadin.documentTitleSignal.value; }); + +export default function Layout() { + const navigate = useNavigate(); + const location = useLocation(); + vaadin.documentTitleSignal.value = useViewConfig()?.title ?? ''; + + const { state, logout } = useAuth(); + + // @ts-ignore + const userName : string = state.user?.name; + + async function doLogout() { + await logout(); + // Workaround for https://github.com/vaadin/hilla/issues/2235 + window.location.reload(); + } + + return ( + +
+
+

Hybrid Example With Stateful + Auth

+ navigate(path!)} + location={location}> + { + createMenuItems().filter(({to}) => { + return to.startsWith("hilla") || to.startsWith("/hilla"); + }).map(({to, icon, title}) => ( + + {icon && } + {title} + + )) + } + +
+
+ {state.user ? ( + <> +
+ + {userName} +
+ + + ) : ( + + + + )} +
+ +
+ + +

+ {vaadin.documentTitleSignal} +

+ + +
+ ); +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/admin.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/admin.tsx new file mode 100644 index 000000000..85c9fa924 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/admin.tsx @@ -0,0 +1,21 @@ +import { VerticalLayout } from "@vaadin/react-components"; +import type { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import { useState } from "react"; + +export const config: ViewConfig = { + menu: { + title: "Hello Admin", + }, + loginRequired: true, + rolesAllowed: ['ROLE_ADMIN'], +}; + +export default function HelloReact() { + const [name, setName] = useState(""); + + return ( + + Hello ADMIN of the system. + + ); +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/hello-react.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/hello-react.tsx new file mode 100644 index 000000000..a567c6f01 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/hello-react.tsx @@ -0,0 +1,19 @@ +import { VerticalLayout } from "@vaadin/react-components"; +import type { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import { useState } from "react"; + +export const config: ViewConfig = { + menu: { + title: "Hello World React", + } +}; + +export default function User() { + const [name, setName] = useState(""); + + return ( + + Hello to everyone. + + ); +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/user.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/user.tsx new file mode 100644 index 000000000..6f7836d32 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/hilla/user.tsx @@ -0,0 +1,21 @@ +import { VerticalLayout } from "@vaadin/react-components"; +import type { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import { useState } from "react"; + +export const config: ViewConfig = { + menu: { + title: "Hello User", + }, + loginRequired: true, + rolesAllowed: ['ROLE_USER'], +}; + +export default function HelloReact() { + const [name, setName] = useState(""); + + return ( + + Hello USER of the system. + + ); +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/login.tsx b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/login.tsx new file mode 100644 index 000000000..17dc22ea9 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/frontend/views/login.tsx @@ -0,0 +1,58 @@ +import { LoginOverlay } from '@vaadin/react-components'; +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from 'Frontend/auth'; +import { ViewConfig } from "@vaadin/hilla-file-router/types.js"; + +export const config: ViewConfig = { + menu: { exclude: true} +} + +interface NavigateAndReloadProps { + to: string; +} + +const NavigateAndReload : React.FC = ({ to }) => { + const navigate = useNavigate(); + + useEffect(() => { + navigate(to, { replace: true }); + // reload a page on log in to update the menu items + window.location.reload(); + }, [navigate, to]); + + return null; +}; + +/** + * Login views in Hilla + */ +export default function Login() { + const { state, login } = useAuth(); + const [hasError, setError] = useState(); + const [url, setUrl] = useState(); + + if (state.user && url) { + const path = new URL(url, document.baseURI).pathname; + return ; + } + + return ( + { + const { defaultUrl, error, redirectUrl } = await login(username, password); + + if (error) { + setError(true); + } else { + setUrl(redirectUrl ?? defaultUrl ?? '/'); + } + }} + /> + ); +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/Application.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/Application.java new file mode 100644 index 000000000..1f6c552be --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/Application.java @@ -0,0 +1,37 @@ +package com.vaadin.platform.react.test; + +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.server.PWA; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; + +import com.vaadin.flow.server.auth.DefaultMenuAccessControl; +import com.vaadin.flow.server.auth.MenuAccessControl; +import com.vaadin.flow.theme.Theme; + +/** + * The entry point of the Spring Boot application. + * + * Use the * and some desktop browsers. + * + */ +@SpringBootApplication +@Theme(value = "react-test") +@PWA(name = "react-test", shortName = "react-test", offlineResources = {"images/logo.png"}) +public class Application extends SpringBootServletInitializer implements AppShellConfigurator { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public MenuAccessControl customMenuAccessControl() { + var menuAccessControl = new DefaultMenuAccessControl(); + menuAccessControl.setPopulateClientSideMenu( + MenuAccessControl.PopulateClientMenu.ALWAYS); + return menuAccessControl; + } +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/Role.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/Role.java new file mode 100644 index 000000000..ceaa17620 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/Role.java @@ -0,0 +1,6 @@ +package com.vaadin.platform.react.test.security; + +public class Role { + public static final String USER = "USER"; + public static final String ADMIN = "ADMIN"; +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/SecurityConfig.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/SecurityConfig.java new file mode 100644 index 000000000..d65b99928 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/SecurityConfig.java @@ -0,0 +1,56 @@ +package com.vaadin.platform.react.test.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import com.vaadin.flow.spring.security.VaadinWebSecurity; +import com.vaadin.hilla.route.RouteUtil; + +@EnableWebSecurity +@Configuration +public class SecurityConfig extends VaadinWebSecurity { + private final RouteUtil routeUtil; + + public SecurityConfig(RouteUtil routeUtil) { + this.routeUtil = routeUtil; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(registry -> { + registry.requestMatchers(new AntPathRequestMatcher("/")).permitAll(); + registry.requestMatchers(routeUtil::isRouteAllowed).permitAll(); + }); + super.configure(http); + setLoginView(http, "/login", "/"); + } + + @Override + protected void configure(WebSecurity web) throws Exception { + super.configure(web); + web.ignoring().requestMatchers(new AntPathRequestMatcher("/images/**")); + } + + @Bean + public UserDetailsService users() { + UserDetails user = User.builder() + .username("user") + .password("{noop}user") + .roles(Role.USER) + .build(); + UserDetails admin = User.builder() + .username("admin") + .password("{noop}admin") + .roles(Role.ADMIN) + .build(); + return new InMemoryUserDetailsManager(user, admin); + } +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/UserInfo.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/UserInfo.java new file mode 100644 index 000000000..d0ac925cb --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/UserInfo.java @@ -0,0 +1,32 @@ +package com.vaadin.platform.react.test.security; + +import java.util.Collection; +import java.util.Collections; + +import com.vaadin.hilla.Nonnull; + +/** + * User information used in client-side authentication and authorization. + * To be saved in browsers’ LocalStorage for offline support. + */ +public final class UserInfo { + + @Nonnull + private final String name; + @Nonnull + private final Collection<@Nonnull String> authorities; + + public UserInfo(String name, Collection authorities) { + this.name = name; + this.authorities = Collections.unmodifiableCollection(authorities); + } + + public String getName() { + return name; + } + + public Collection getAuthorities() { + return authorities; + } + +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/UserInfoService.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/UserInfoService.java new file mode 100644 index 000000000..1dd61c9b1 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/security/UserInfoService.java @@ -0,0 +1,34 @@ +package com.vaadin.platform.react.test.security; + +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.security.PermitAll; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.vaadin.hilla.BrowserCallable; + +/** + * Server endpoint that provides information about the current user to the + * Hilla's client-side authentication. + */ +@BrowserCallable +public class UserInfoService { + + @PermitAll + @Nonnull + public UserInfo getUserInfo() { + Authentication auth = SecurityContextHolder.getContext() + .getAuthentication(); + + final List authorities = auth.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + + return new UserInfo(auth.getName(), authorities); + } + +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/AccessLayout.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/AccessLayout.java new file mode 100644 index 000000000..ffdfeeb60 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/AccessLayout.java @@ -0,0 +1,16 @@ +package com.vaadin.platform.react.test.views; + +import jakarta.annotation.security.RolesAllowed; + +import com.vaadin.flow.router.Layout; +import com.vaadin.flow.spring.security.AuthenticationContext; +import com.vaadin.platform.react.test.security.Role; + +@Layout("/flow/access") +@RolesAllowed({ Role.USER, Role.ADMIN }) +public class AccessLayout extends FlowLayout { + + public AccessLayout(AuthenticationContext authenticationContext) { + super(authenticationContext); + } +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/AdminView.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/AdminView.java new file mode 100644 index 000000000..05015d67c --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/AdminView.java @@ -0,0 +1,19 @@ +package com.vaadin.platform.react.test.views; + +import jakarta.annotation.security.RolesAllowed; + +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Menu; +import com.vaadin.flow.router.Route; +import com.vaadin.platform.react.test.security.Role; + +@Route("flow/access/admin") +@RolesAllowed(Role.ADMIN) +@Menu(title = "Admin view") +public class AdminView extends VerticalLayout { + + public AdminView() { + add(new Span("Administator only view")); + } +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowHillaView.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowHillaView.java new file mode 100644 index 000000000..605e9e314 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowHillaView.java @@ -0,0 +1,32 @@ +package com.vaadin.platform.react.test.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Menu; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@Route("hilla/flow") +@PageTitle("Hello Hilla") +@Menu(title = "Flow in hilla") +public class FlowHillaView extends HorizontalLayout { + + private TextField name; + private Button sayHello; + + public FlowHillaView() { + setId("flow-hilla"); + setPadding(true); + setSpacing(true); + name = new TextField("Your name for Flow"); + sayHello = new Button("Say hello"); + add(name, sayHello); + setVerticalComponentAlignment(Alignment.END, name, sayHello); + sayHello.addClickListener(e -> { + Notification.show("Hello " + name.getValue()); + }); + } + +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowLayout.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowLayout.java new file mode 100644 index 000000000..49c45a8f2 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowLayout.java @@ -0,0 +1,100 @@ +package com.vaadin.platform.react.test.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.button.Button; +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.orderedlayout.Scroller; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.Layout; +import com.vaadin.flow.router.RoutePrefix; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.server.menu.MenuConfiguration; +import com.vaadin.flow.spring.security.AuthenticationContext; +import com.vaadin.flow.theme.lumo.LumoUtility; + +@Layout("/flow") +@RoutePrefix("flow") +@AnonymousAllowed +public class FlowLayout extends AppLayout { + public static final String LOGIN_BUTTON_ID = "login-button"; + private final AuthenticationContext authenticationContext; + + private H1 viewTitle; + + public FlowLayout(AuthenticationContext authenticationContext) { + this.authenticationContext = authenticationContext; + 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("Flow menu"); + 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(); + + MenuConfiguration.getMenuEntries().forEach(menuEntry -> { + if (menuEntry.path().startsWith("flow") || + menuEntry.path().startsWith("/flow")) { + nav.addItem( + new SideNavItem(menuEntry.title(), menuEntry.path())); + } + }); + + return nav; + } + + private Footer createFooter() { + Footer layout = new Footer(); + Button login; + + if (authenticationContext.isAuthenticated()) { + login = new Button("Logout", e -> { + authenticationContext.logout(); + e.getSource().getUI().get().getPage().reload(); + }); + } else { + login = new Button("Sign in", + e -> e.getSource().getUI().get().navigate("login")); + } + login.setId(LOGIN_BUTTON_ID); + layout.add(login); + + return layout; + } + + @Override + protected void afterNavigation() { + super.afterNavigation(); + viewTitle.setText(getCurrentPageTitle()); + } + + private String getCurrentPageTitle() { + return MenuConfiguration.getPageHeader(getContent()).orElse(""); + } +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowMainView.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowMainView.java new file mode 100644 index 000000000..503b4dae6 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/FlowMainView.java @@ -0,0 +1,29 @@ +package com.vaadin.platform.react.test.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; +import com.vaadin.flow.server.auth.AnonymousAllowed; + +@Route("flow") +@AnonymousAllowed +public class FlowMainView extends VerticalLayout { + public FlowMainView() { + Span span = new Span("Flow root view for menu!"); + span.setId("flow-main"); + add(span); + + RouterLink flow = new RouterLink("Flow with RouterLink", + PublicView.class); + flow.setId("flow-link"); + + Button flowButton = new Button("Flow with Button", + e -> e.getSource().getUI().get() + .navigate(PublicView.class)); + flowButton.setId("flow-button"); + + add(flow, flowButton); + } +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/LayoutSecuredView.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/LayoutSecuredView.java new file mode 100644 index 000000000..cca7d62aa --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/LayoutSecuredView.java @@ -0,0 +1,21 @@ +package com.vaadin.platform.react.test.views; + +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; + +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Menu; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.platform.react.test.security.Role; + +@Route("flow/access/layout") +@AnonymousAllowed +@Menu(title = "Layout secured") +public class LayoutSecuredView extends VerticalLayout { + + public LayoutSecuredView() { + add(new Span("Only available with layout access")); + } +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/PublicView.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/PublicView.java new file mode 100644 index 000000000..553546560 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/PublicView.java @@ -0,0 +1,34 @@ +package com.vaadin.platform.react.test.views; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Menu; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; + +@Route("flow/public") +@PageTitle("Public View") +@Menu(title = "Flow Public") +@AnonymousAllowed +public class PublicView extends HorizontalLayout { + + private TextField name; + private Button sayHello; + + public PublicView() { + setId("flow-hello"); + setPadding(true); + setSpacing(true); + name = new TextField("Your name"); + sayHello = new Button("Say hello"); + add(name, sayHello); + setVerticalComponentAlignment(Alignment.END, name, sayHello); + sayHello.addClickListener(e -> { + Notification.show("Hello " + name.getValue()); + }); + } + +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/UserView.java b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/UserView.java new file mode 100644 index 000000000..98936cc6a --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/main/java/com/vaadin/platform/react/test/views/UserView.java @@ -0,0 +1,19 @@ +package com.vaadin.platform.react.test.views; + +import jakarta.annotation.security.RolesAllowed; + +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Menu; +import com.vaadin.flow.router.Route; +import com.vaadin.platform.react.test.security.Role; + +@Route("flow/access/user") +@RolesAllowed(Role.USER) +@Menu(title = "User view") +public class UserView extends VerticalLayout { + + public UserView() { + add(new Span("User only view")); + } +} diff --git a/vaadin-platform-react-hybrid-security-test/src/main/resources/META-INF/resources/icons/icon.png b/vaadin-platform-react-hybrid-security-test/src/main/resources/META-INF/resources/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6fde5c10d44ec14b621ef75e6b283f564c017f38 GIT binary patch literal 15994 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 getMenuElement(String label) { + List items = $(SideNavElement.class).first() + .getItems(); + return items.stream().filter(item -> item.getLabel().equals(label)) + .findFirst(); + } + + /** + * Wait for element else fail with message. + * + * @param message failure message + * @param by By for locating element + */ + protected void waitForElement(String message, By by) { + try { + waitUntil(ExpectedConditions.presenceOfElementLocated(by)); + } catch (TimeoutException te) { + Assert.fail(message); + } + } + + protected void login(String username, String password) { + LoginFormElement loginForm = $(LoginFormElement.class).first(); + loginForm.getUsernameField().setValue(username); + loginForm.getPasswordField().setValue(password); + loginForm.getSubmitButton().click(); + waitUntil(ExpectedConditions.stalenessOf(loginForm)); + } +} \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-security-test/src/test/java/com/vaadin/platform/react/test/FlowMainLayoutIT.java b/vaadin-platform-react-hybrid-security-test/src/test/java/com/vaadin/platform/react/test/FlowMainLayoutIT.java new file mode 100644 index 000000000..6c58c5456 --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/test/java/com/vaadin/platform/react/test/FlowMainLayoutIT.java @@ -0,0 +1,111 @@ +package com.vaadin.platform.react.test; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; + +import com.vaadin.flow.component.button.testbench.ButtonElement; +import com.vaadin.flow.component.sidenav.testbench.SideNavItemElement; + +import static com.vaadin.platform.react.test.views.FlowLayout.LOGIN_BUTTON_ID; + +public class FlowMainLayoutIT extends AbstractPlatformTest { + + @Test + public void anonymousUser_loginButtonAndOnlyAlwaysAllowItems() { + Assert.assertTrue("Login button should be visible", + $(ButtonElement.class).id(LOGIN_BUTTON_ID).isDisplayed()); + Assert.assertEquals("Login button should say 'Sign in''", + $(ButtonElement.class).id(LOGIN_BUTTON_ID).getText(), + "Sign in"); + + Assert.assertEquals("Only one route should be available", 1, + $(SideNavItemElement.class).all().size()); + + Assert.assertTrue("Visible element should be 'Flow Public'", + getMenuElement("Flow Public").isPresent()); + } + + @Test + public void loginUser_forAllAndUserViewsAvailable_logout_onlyPublicAvailable() { + $(ButtonElement.class).id(LOGIN_BUTTON_ID).click(); + login("user", "user"); + + // If Login has navigated us to / return to Flow + if ($(ButtonElement.class).withId("flow").exists()) { + $(ButtonElement.class).id("flow").click(); + } + + Assert.assertEquals( + "Login button should say 'logout' as user is logged in", + "Logout", $(ButtonElement.class).id(LOGIN_BUTTON_ID).getText()); + + Assert.assertEquals("Four routes should be available", 4, + $(SideNavItemElement.class).all().size()); + + Assert.assertFalse("Admin view should not be present", + getMenuElement("Admin view").isPresent()); + + Assert.assertTrue( + "Access all view with layout using roles should be available", + getMenuElement("Layout secured").isPresent()); + + // Logout + $(ButtonElement.class).id(LOGIN_BUTTON_ID).click(); + + waitForElement("No login button found", By.id(LOGIN_BUTTON_ID)); + + + Assert.assertEquals("Only one route should be available", 1, + $(SideNavItemElement.class).all().size()); + + Assert.assertTrue("Visible element should be 'Flow Public'", + getMenuElement("Flow Public").isPresent()); + } + + + @Test + public void loginAdmin_forAllAndAdminViewsAvailable() { + $(ButtonElement.class).id(LOGIN_BUTTON_ID).click(); + login("admin", "admin"); + + // If Login has navigated us to / return to Flow + if ($(ButtonElement.class).withId("flow").exists()) { + $(ButtonElement.class).id("flow").click(); + } + + Assert.assertEquals( + "Login button should say 'logout' as user is logged in", + "Logout", $(ButtonElement.class).id(LOGIN_BUTTON_ID).getText()); + + Assert.assertEquals("Four routes should be available", 4, + $(SideNavItemElement.class).all().size()); + + Assert.assertTrue("Admin view should be present", + getMenuElement("Admin view").isPresent()); + + Assert.assertFalse("User view should not be present", + getMenuElement("User view").isPresent()); + + Assert.assertTrue( + "Access all view with layout using roles should be available", + getMenuElement("Layout secured").isPresent()); + + // Logout + $(ButtonElement.class).id(LOGIN_BUTTON_ID).click(); + + waitForElement("No login button found", By.id(LOGIN_BUTTON_ID)); + + + Assert.assertEquals("Only one route should be available", 1, + $(SideNavItemElement.class).all().size()); + + Assert.assertTrue("Visible element should be 'Flow Public'", + getMenuElement("Flow Public").isPresent()); + } + + @Override + protected String getTestPath() { + return "/flow"; + } +} diff --git a/vaadin-platform-react-hybrid-security-test/src/test/java/com/vaadin/platform/react/test/HillaMainLayoutIT.java b/vaadin-platform-react-hybrid-security-test/src/test/java/com/vaadin/platform/react/test/HillaMainLayoutIT.java new file mode 100644 index 000000000..79a26fb4a --- /dev/null +++ b/vaadin-platform-react-hybrid-security-test/src/test/java/com/vaadin/platform/react/test/HillaMainLayoutIT.java @@ -0,0 +1,103 @@ +package com.vaadin.platform.react.test; + + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.support.ui.ExpectedConditions; + +import com.vaadin.flow.component.button.testbench.ButtonElement; +import com.vaadin.flow.component.orderedlayout.testbench.VerticalLayoutElement; +import com.vaadin.flow.component.sidenav.testbench.SideNavItemElement; + +import static com.vaadin.platform.react.test.views.FlowLayout.LOGIN_BUTTON_ID; + +public class HillaMainLayoutIT extends AbstractPlatformTest { + + @Test + public void anonymousUser_loginButtonAndOnlyAlwaysAllowItems() { + Assert.assertTrue("Login button should be visible", + $(ButtonElement.class).id(LOGIN_BUTTON_ID).isDisplayed()); + Assert.assertEquals("Login button should say 'Sign in''", + $(ButtonElement.class).id(LOGIN_BUTTON_ID).getText(), + "Sign in"); + + Assert.assertEquals("Only one route should be available", 1, + $(SideNavItemElement.class).all().size()); + + Assert.assertTrue("Visible element should be 'Flow Public'", + getMenuElement("Hello World React").isPresent()); + } + + @Test + public void loginUser_forAllAndUserViewsAvailable_logout_onlyPublicAvailable() { + $(ButtonElement.class).id(LOGIN_BUTTON_ID).click(); + login("user", "user"); + + // If Login has navigated us to / return to hilla + if ($(ButtonElement.class).withId("hilla").exists()) { + $(ButtonElement.class).id("hilla").click(); + } + + Assert.assertEquals( + "Login button should say 'sign out' as user is logged in", + "Sign out", + $(ButtonElement.class).id(LOGIN_BUTTON_ID).getText()); + + Assert.assertEquals("Two routes should be available", 2, + $(SideNavItemElement.class).all().size()); + + Assert.assertFalse("Admin view should not be present", + getMenuElement("Hello Admin").isPresent()); + + Assert.assertTrue( + "User view should be present", + getMenuElement("Hello User").isPresent()); + + // Logout + ButtonElement loginButton = $(ButtonElement.class).id(LOGIN_BUTTON_ID); + loginButton.click(); + + // Wait for page reload that makes the button reference stale. + waitUntil(ExpectedConditions.stalenessOf(loginButton)); + + Assert.assertEquals("Only one route should be available", 1, + $(SideNavItemElement.class).all().size()); + + Assert.assertTrue("Visible element should be 'Flow Public'", + getMenuElement("Hello World React").isPresent()); + } + + + @Test + public void loginAdmin_forAllAndAdminViewsAvailable() { + $(ButtonElement.class).id(LOGIN_BUTTON_ID).click(); + login("admin", "admin"); + + // If Login has navigated us to / return to hilla + if ($(ButtonElement.class).withId("hilla").exists()) { + $(ButtonElement.class).id("hilla").click(); + } + + Assert.assertEquals( + "Login button should say 'sign out' as user is logged in", + "Sign out", + $(ButtonElement.class).id(LOGIN_BUTTON_ID).getText()); + + Assert.assertEquals("Two routes should be available", 2, + $(SideNavItemElement.class).all().size()); + + Assert.assertTrue("Admin view should be present", + getMenuElement("Hello Admin").isPresent()); + + Assert.assertFalse( + "User view should not be present", + getMenuElement("Hello User").isPresent()); + + } + + @Override + protected String getTestPath() { + return "/hilla"; + } +} From 40b1fcedb51428a2c915e377bd321cfd18f46c5e Mon Sep 17 00:00:00 2001 From: Zhe Sun <31067185+ZheSun88@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:13:09 +0300 Subject: [PATCH 3/6] chore: correct the vaadin-platform-react-hybrid-security-test module (#6923) * chore: correct the vaadin-platform-react-hybrid-security-test module * clean the pom --- .../pom.xml | 29 ++++--------------- vaadin-platform-react-hybrid-test/pom.xml | 8 ----- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/vaadin-platform-react-hybrid-security-test/pom.xml b/vaadin-platform-react-hybrid-security-test/pom.xml index 796b6393f..4773d81f3 100644 --- a/vaadin-platform-react-hybrid-security-test/pom.xml +++ b/vaadin-platform-react-hybrid-security-test/pom.xml @@ -7,16 +7,10 @@ vaadin-platform-parent 24.6-SNAPSHOT - - - - - - - vaadin-platform-react-hybrid-test-prod + vaadin-platform-react-hybrid-security-test-prod war - Vaadin Platform React Hybrid (flow and hilla views) Tests (Production Mode) - Vaadin Platform React Hybrid (flow and hilla views) Tests (Production Mode) + Vaadin Platform React Hybrid (flow and hilla views) security Tests (Production Mode) + Vaadin Platform React Hybrid (flow and hilla views) security Tests (Production Mode) https://vaadin.com 17 @@ -31,14 +25,14 @@ com.vaadin vaadin-bom - 24.6-SNAPSHOT + ${project.version} pom import org.springframework.boot spring-boot-dependencies - 3.3.4 + ${spring.boot.version} pom import @@ -112,7 +106,6 @@ com.vaadin vaadin-testbench test - 24.6-SNAPSHOT junit @@ -126,18 +119,6 @@ slf4j-simple - - io.github.bonigarcia - webdrivermanager - 4.4.3 - test - - - org.jsoup - jsoup - - - diff --git a/vaadin-platform-react-hybrid-test/pom.xml b/vaadin-platform-react-hybrid-test/pom.xml index e633faa7f..a9fe5468a 100644 --- a/vaadin-platform-react-hybrid-test/pom.xml +++ b/vaadin-platform-react-hybrid-test/pom.xml @@ -98,7 +98,6 @@ com.vaadin vaadin-testbench test - ${project.version} junit @@ -111,13 +110,6 @@ org.slf4j slf4j-simple - - - io.github.bonigarcia - webdrivermanager - 4.4.3 - test - From 8ffcb0c4f5b158177c918112dd302d7268814f2a Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Thu, 24 Oct 2024 15:33:35 +0200 Subject: [PATCH 4/6] chore: exclude flow artifacts from requireUpperBoundDeps enforcer rule [skip ci] (#6921) * chore: exclude flow artifacts from requireUpperBoundDeps enforcer rule * exclude com.vaadin group * exclude specific artifacts --------- Co-authored-by: Zhe Sun <31067185+ZheSun88@users.noreply.github.com> --- vaadin/pom.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vaadin/pom.xml b/vaadin/pom.xml index 90797f98b..399ef2746 100644 --- a/vaadin/pom.xml +++ b/vaadin/pom.xml @@ -75,6 +75,9 @@ + com.vaadin:flow-server + com.vaadin:flow-html-components + com.vaadin:flow-data org.slf4j:slf4j-api From 16ad1243262c01ab8fe8e25dad7eb3cc4eedb4a6 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Fri, 25 Oct 2024 08:27:26 +0200 Subject: [PATCH 5/6] chore: make hybrid dev module a provided dependency (#6918) vaadin-dev-bundle requires vaadin-hybrid-dev-bundle only to extract the hybrid package-lock.json file. It necessary to have the hybrid bundle module as a project dependency, to make sure modules are executed in the correct order by maven reactor. However, the scope of the hybrid dependency must be provided to prevent the flow maven plugin to find its stats.json in classpath and then refusing to create a new dev bundle. Co-authored-by: Zhe Sun <31067185+ZheSun88@users.noreply.github.com> --- scripts/generator/templates/template-dev-bundle-pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/generator/templates/template-dev-bundle-pom.xml b/scripts/generator/templates/template-dev-bundle-pom.xml index 349ce3727..19de6b2dc 100644 --- a/scripts/generator/templates/template-dev-bundle-pom.xml +++ b/scripts/generator/templates/template-dev-bundle-pom.xml @@ -25,10 +25,17 @@ + ${project.groupId} vaadin-hybrid-dev-bundle ${project.version} + provided true From f9f27f122f38f5e224f799599eb98c4a94e91205 Mon Sep 17 00:00:00 2001 From: Zhe Sun <31067185+ZheSun88@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:07:04 +0300 Subject: [PATCH 6/6] chore: upgrade springboot to 3.3.5 (#6927) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f44a8c399..f934afbff 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 17 17 - 3.3.4 + 3.3.5 5.9.1 11.0.13