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 000000000..6fde5c10d Binary files /dev/null and b/vaadin-platform-react-hybrid-test/src/main/resources/META-INF/resources/icons/icon.png differ diff --git a/vaadin-platform-react-hybrid-test/src/main/resources/META-INF/resources/images/logo.png b/vaadin-platform-react-hybrid-test/src/main/resources/META-INF/resources/images/logo.png new file mode 100644 index 000000000..d9215808b Binary files /dev/null and b/vaadin-platform-react-hybrid-test/src/main/resources/META-INF/resources/images/logo.png differ diff --git a/vaadin-platform-react-hybrid-test/src/main/resources/application.properties b/vaadin-platform-react-hybrid-test/src/main/resources/application.properties new file mode 100644 index 000000000..e9453ac6d --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/resources/application.properties @@ -0,0 +1,8 @@ +server.port=${PORT:8080} +logging.level.org.atmosphere = warn +spring.mustache.check-template-location = false +vaadin.devmode.liveReload.enabled = false + +# To improve the performance during development. +# For more information https://vaadin.com/docs/flow/spring/tutorial-spring-configuration.html#special-configuration-parameters +# vaadin.whitelisted-packages= org/vaadin/example diff --git a/vaadin-platform-react-hybrid-test/src/main/resources/banner.txt b/vaadin-platform-react-hybrid-test/src/main/resources/banner.txt new file mode 100644 index 000000000..3044d1394 --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/main/resources/banner.txt @@ -0,0 +1,5 @@ +,------. ,--. ,--. ,--. +| .--. ' ,---. ,--,--. ,---.,-' '-.,-----.,-' '-. ,---. ,---.,-' '-. +| '--'.'| .-. :' ,-. || .--''-. .-''-----''-. .-'| .-. :( .-''-. .-' +| |\ \ \ --.\ '-' |\ `--. | | | | \ --..-' `) | | +`--' '--' `----' `--`--' `---' `--' `--' `----'`----' `--' \ No newline at end of file diff --git a/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/AbstractPlatformTest.java b/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/AbstractPlatformTest.java new file mode 100644 index 000000000..9327ee22d --- /dev/null +++ b/vaadin-platform-react-hybrid-test/src/test/java/com/vaadin/platform/react/test/AbstractPlatformTest.java @@ -0,0 +1,131 @@ +/* + * 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 java.util.List; +import java.util.Optional; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.openqa.selenium.By; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.sidenav.testbench.SideNavElement; +import com.vaadin.flow.component.sidenav.testbench.SideNavItemElement; +import com.vaadin.testbench.IPAddress; +import com.vaadin.testbench.parallel.ParallelTest; +import com.vaadin.testbench.parallel.SauceLabsIntegration; + +public abstract class AbstractPlatformTest extends ParallelTest { + + public static final int SERVER_PORT = Integer.parseInt( + System.getProperty("serverPort", "8080")); + + static String hostName; + static boolean isSauce; + static boolean isHub; + static boolean isLocal; + + static Logger getLogger() { + return LoggerFactory.getLogger(AbstractPlatformTest.class); + } + + @BeforeClass + public static void setupClass() { + isSauce = SauceLabsIntegration.isConfiguredForSauceLabs(); + String hubHost = System.getProperty( + "com.vaadin.testbench.Parameters.hubHostname"); + isHub = !isSauce && hubHost != null && !hubHost.isEmpty(); + + hostName = isHub ? IPAddress.findSiteLocalAddress() : "localhost"; + getLogger().info("Running Tests app-url=http://{}:{} mode={}", hostName, + SERVER_PORT, isSauce ? + "SAUCE (user:" + SauceLabsIntegration.getSauceUser() + + ")" : isHub ? "HUB (hub-host:" + hubHost + ")" : + "LOCAL (chromedriver)"); + } + + @Before + public void setUp() { + getDriver().get(getRootURL() + getTestPath()); + } + + /** + * Gets the absolute path to the test, starting with a "/". + * + * @return he path to the test, appended to {@link #getRootURL()} for the + * full test URL. + */ + protected abstract String getTestPath(); + + /** + * Returns the URL to the root of the server, e.g. "http://localhost:8888". + * + * @return the URL to the root + */ + protected String getRootURL() { + return "http://" + getDeploymentHostname() + ":" + getDeploymentPort(); + } + + /** + * Used to determine what port the test is running on. + * + * @return The port the test is running on, by default 8080 + */ + protected int getDeploymentPort() { + return SERVER_PORT; + } + + /** + * Used to determine what URL to initially open for the test. + * + * @return the host name of development server + */ + protected String getDeploymentHostname() { + return hostName; + } + + /** + * Get the sideNavItem with given label + * + * @param label label to look for + * @return sidenav with label if found + */ + protected Optional 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"; + } +}