Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/styles/config/vocabularies/Docs/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ Valo
[vV]iewports?
Viritin
Vite
Vitest
VMware
Vue
VU
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Vaadin recommends another tool, at least for part of the application, when one o

*Search engine ranking.* Vaadin builds both Flow and React views in the browser with JavaScript, and doesn't render pages to HTML on the server for search engines. A Flow view also keeps a UI on the server for every visitor, crawlers included. Keep content pages on a content management system or a static site, and link from them to your application.

*Mobile-first or offline-first use.* Vaadin's layouts adapt to the screen size in the browser. However, every interaction that runs Java code adds the latency of the mobile network, and a Flow view stops working when the connection drops. React views can keep working offline, but you write the code that stores data in the browser and synchronizes it with the server, as described in <<{articles}/hilla/guides/client-caching#,Client-Side Data Caching>>. If most of the application has to work offline, most of it ends up as React views, and the benefits of writing the UI in Java no longer apply.
*Mobile-first or offline-first use.* Vaadin's layouts adapt to the screen size in the browser. However, every interaction that runs Java code adds the latency of the mobile network, and a Flow view stops working when the connection drops. React views can keep working offline, but you write the code that stores data in the browser and synchronizes it with the server, as described in <<{articles}/building-apps/react/work-offline#,Work Offline>>. If most of the application has to work offline, most of it ends up as React views, and the benefits of writing the UI in Java no longer apply.

*A highly custom user interface.* Vaadin is designed for applications whose views follow one consistent design. The default theme styles the UI components without any CSS of your own, and you can <<{articles}/styling#,style>> them further, for example to match a brand. When a designer specifies each screen in detail, and the user interface is the product, a hand-written frontend gives more control. React views give that control within a view, and can still use Vaadin's UI components.

Expand Down
2 changes: 1 addition & 1 deletion articles/building-apps/forms-data/add-grid/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ section_outline::[]

For detailed information about binding data sets to UI components in Vaadin, see the <</flow/binding-data/data-provider#,Data Provider>> reference guide.

The guides use Flow views. For grids in React views, see the <</hilla/guides/data-grids#,Data Grids>> reference guide.
The guides use Flow views. For grids in React views, see <<{articles}/components/auto-grid#,Auto Grid>>, which loads its data from a browser-callable service.
5 changes: 4 additions & 1 deletion articles/building-apps/react/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,8 @@ This section covers:
- <<navigate#,Navigation and Layouts>> -- link between views (including to and from Flow views), pass route and query parameters, and wrap views in shared layouts.
- <<call-services#,Call Java Services>> -- make a Java service browser-callable and call it from a React view in a type-safe way.
- <<security#,Secure React Views>> -- require authentication, restrict access by role, add login and logout, and protect the services behind your views.
- <<work-offline#,Work Offline>> -- load the application without the server, and keep the data of a view available.
- <<upload-download#,Upload and Download Files>> -- upload files with the Upload component, and let users download them.
- <<test-views#,Test React Views>> -- test views in a browser with Vitest and React Testing Library, with mocked services.

For the reference documentation of React views, see <<{articles}/flow/react-views#,React Views>> in the Flow Reference. For form building, validation, and other in-depth React topics, see the <<{articles}/hilla/guides#,Hilla reference guides>>.
For the reference documentation of React views, including form binding and browser-callable services, see <<{articles}/flow/react-views#,React Views>> in the Flow Reference.
208 changes: 208 additions & 0 deletions articles/building-apps/react/test-views.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
title: Test React Views
page-title: How to test React views in a Vaadin application with Vitest
description: Test React views in a browser with Vitest and React Testing Library, without a running server.
meta-description: Test Vaadin React views with Vitest browser mode and React Testing Library - set up Vitest, render views, simulate users, and mock browser-callable services.
order: 70
---


= Test React Views
:toclevels: 2

React views run in the browser, so you test them with JavaScript tools. This guide uses https://vitest.dev/guide/browser/[Vitest in browser mode], which runs the tests in a real browser, and https://testing-library.com/docs/react-testing-library/intro/[React Testing Library], which renders a view and works with it like a user. The tests replace the browser-callable services with mocks, so they don't need a running server.

Test the services themselves with Java tests, as described in <<../testing#,Testing>>. To test the whole application in a browser, see <<{articles}/flow/testing/end-to-end#,End-to-End Testing>>.


== Setting Up Vitest

Install Vitest, its Playwright provider, and React Testing Library, and then the Chromium browser that the tests run in:

[source,bash]
----
npm install --save-dev vitest @vitest/browser-playwright @testing-library/react @testing-library/dom @testing-library/user-event
----

[source,bash]
----
npx playwright install chromium
----

Create a [filename]`vitest.config.ts` file in the project folder:

[source,typescript]
.[filename]`vitest.config.ts`
----
import { fileURLToPath } from 'node:url';
import babel from '@rolldown/plugin-babel';
import react from '@vitejs/plugin-react';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';

export default defineConfig({
plugins: [
react(),
babel({ // <1>
include: '**/*.tsx',
plugins: [['module:@preact/signals-react-transform', { mode: 'all' }]],
}),
],
optimizeDeps: {
include: ['@preact/signals-react/runtime'], // <2>
},
resolve: {
alias: {
Frontend: fileURLToPath(new URL('./src/main/frontend', import.meta.url)), // <3>
},
},
test: {
include: ['src/test/frontend/**/*.test.tsx'],
globals: true, // <4>
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [{ browser: 'chromium' }],
},
},
});
----
<1> The signals transform makes a component render again when a signal that it reads changes, like in the application. Vaadin already adds these packages to [filename]`package.json`.
<2> The transform adds an import that Vite doesn't find in advance. Without this line, a run without a Vite cache, such as on a CI server, fails.
<3> Resolves the `Frontend/` imports of the views.
<4> Lets React Testing Library clean up the page after each test.

Add a script that runs the tests to [filename]`package.json`:

[source,json]
.[filename]`package.json`
----
"scripts": {
"test": "vitest"
}
----

For type checking in your editor, add a [filename]`tsconfig.json` file to the folder of the tests. The [filename]`tsconfig.json` in the project folder covers only [filename]`src/main/frontend`, and Vaadin may replace it, so don't change it for the tests:

[source,json]
.[filename]`src/test/frontend/tsconfig.json`
----
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"types": ["@vitest/browser-playwright"]
},
"include": ["**/*", "../../../types.d.ts"]
}
----

When a test fails, Vitest saves a screenshot in the [filename]`.vitest` folder. Add the folder to [filename]`.gitignore`.


== Generating the Service Clients

The views import the TypeScript clients that Vaadin generates for the browser-callable services, so the tests need them too, even when they mock the services. Vaadin generates the clients when the application starts in development mode. Without starting it, for example on a CI server, generate them with Maven before running the tests:

[source,bash]
----
mvn compile vaadin:generate
----


== Writing a Test

The examples in this guide test the following view, which adds todos through a service:

[source,tsx]
.[filename]`src/main/frontend/views/todo.tsx`
----
include::{root}/frontend/demo/fusion/testing/todo-view.tsx[tags=snippet,indent=0]
----

[source,java]
.[filename]`TodoService.java`
----
include::{root}/src/main/java/com/vaadin/demo/fusion/testing/TodoService.java[tags=snippet,indent=0]
----

Put the tests in [filename]`src/test/frontend`, not in the [filename]`views` folder, because the router treats every file there as a view. The following test renders the view and checks its heading:

[source,tsx]
.[filename]`src/test/frontend/views/todo.test.tsx`
----
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import TodoView from 'Frontend/views/todo';

describe('TodoView', () => {
it('shows the heading', () => {
render(<TodoView />);
expect(screen.getByText('Todos')).toBeInTheDocument();
});
});
----

Run the tests:

[source,bash]
----
npm test
----

In a terminal, Vitest keeps running and runs the tests again when a file changes. To run them once, for example on a CI server, use `npx vitest run`.


== Mocking Services

A view that calls a service in a test would send the call to the test server, which doesn't know the service. Replace the generated clients with mocks by calling `vi.mock()` at the top level of the test file:

[source,tsx]
----
import { TodoService } from 'Frontend/generated/endpoints';
import { beforeEach, vi } from 'vitest';

vi.mock('Frontend/generated/endpoints', () => ({
TodoService: { addTodo: vi.fn() },
}));

beforeEach(() => {
vi.clearAllMocks();
});
----

The mock replaces the module for every test in the file, and for the views that the tests render. It contains only the services and methods that you list. The `beforeEach()` hook clears the recorded calls before each test.

You can't replace a single method with `vi.spyOn(TodoService, 'addTodo')`, because the generated clients are ES modules, which can't be changed.


== Testing User Interaction

The `userEvent` object of React Testing Library types and clicks like a user. The following tests add a todo, and check that the view shows it and sends it to the service:

[source,tsx]
----
import userEvent from '@testing-library/user-event';

it('adds a todo', async () => {
const user = userEvent.setup();
render(<TodoView />);

await user.type(await screen.findByLabelText('New todo'), 'Buy milk');
await user.click(screen.getByRole('button', { name: 'Add' }));

expect(await screen.findByText('Buy milk')).toBeInTheDocument();
});

it('saves the todo through the service', async () => {
const user = userEvent.setup();
render(<TodoView />);

await user.type(await screen.findByLabelText('New todo'), 'Buy milk');
await user.click(screen.getByRole('button', { name: 'Add' }));

expect(TodoService.addTodo).toHaveBeenCalledWith('Buy milk');
});
----

Vaadin components set up their labels after the first render, so find a field by its label with `findByLabelText()`, which waits for it, rather than with `getByLabelText()`. For the other queries and actions, see the https://testing-library.com/docs/react-testing-library/intro/[React Testing Library documentation].
95 changes: 95 additions & 0 deletions articles/building-apps/react/upload-download.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: Upload and Download Files
page-title: How to upload and download files in Vaadin React views
description: Upload files from a React view with the Upload component, and let users download them, through a Spring controller.
meta-description: Upload and download files in Vaadin React views with the Upload component and a Spring controller, protected by Spring Security, with CSRF and size limits.
order: 60
---


= Upload and Download Files
:toclevels: 2

This guide shows how to upload files from a React view with the <<{articles}/components/upload#,Upload>> component, and how to let users download them. Both go through a Spring controller: the Upload component sends each file in a request of its own, and a browser downloads a file from a URL.


== Creating the Controller

Create a Spring controller with one method that receives an uploaded file, and one that sends a file back. The following controller keeps the files in memory and gives each one an ID:

[source,java]
.[filename]`FileController.java`
----
include::{root}/src/main/java/com/vaadin/demo/fusion/files/FileController.java[tags=snippet,indent=0]
----

The [methodname]`upload()` method returns the ID of the file as the response text. The [methodname]`download()` method sends the file with a `Content-Disposition` header, which makes the browser save it under its original name. [classname]`ContentDisposition` encodes names that contain other than ASCII characters.

Keep the controller's path apart from the routes of your views, for example under `/api`. That way, the security rule for the controller doesn't apply to a view.


== Allowing Access to the Controller

Vaadin's security configuration denies the requests that no rule allows, so add a rule for the controller. The following rule lets logged-in users upload and download files:

[source,java]
----
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/files/**").authenticated());
http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
configurer.loginView("/login");
});
return http.build();
}
----

For the other options, see <<{articles}/flow/security/spring/security-configuration#,Security Configuration>> in the reference.


== Uploading Files

Point the `target` of the Upload component to the controller, and send the files as multipart form data. By default, the component sends the raw content of the file, which a [classname]`MultipartFile` parameter doesn't accept. The following view uploads files, and lists the uploaded files with links for downloading them:

[source,tsx]
.[filename]`views/files.tsx`
----
include::{root}/frontend/demo/fusion/files/files-view.tsx[tags=view,indent=0]
----

Spring Security rejects a `POST` request without a CSRF token. Vaadin adds the token to the page in `<meta>` tags, and the `onUploadRequest` handler copies it to each upload request:

[source,tsx]
----
include::{root}/frontend/demo/fusion/files/files-view.tsx[tags=csrf,indent=0]
----

The handler reads the token for each request, because the token changes when the user logs in.

When the upload succeeds, `xhr.responseText` holds the response of the controller, here the ID of the file. The view keeps the ID and the name of each uploaded file in a signal.


=== Limiting the File Size

Spring Boot accepts files of up to 1 MB by default. For a larger file, the server answers with status 413, and the Upload component shows that the file is too large. To accept larger files, set the limits in [filename]`application.properties`:

[source,properties]
----
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
----

To reject a large file before it's uploaded, also set the `maxFileSize` property of the Upload component, in bytes.


== Downloading Files

To let users download a file, link to the download URL of the controller, and add the `download` attribute to the link. The view above does this for each uploaded file:

[source,tsx]
----
include::{root}/frontend/demo/fusion/files/files-view.tsx[tags=download,indent=0]
----

The router ignores links that have the `download` attribute, so the browser downloads the file instead of opening a view.
Loading
Loading