From a61392e7df6e6756e6405c155a55e16a84055072 Mon Sep 17 00:00:00 2001 From: caalador Date: Thu, 24 Oct 2024 14:32:32 +0300 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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