Skip to content

Commit bac04cc

Browse files
committed
Add README explanation and translate tests descriptions
1 parent 4489b4e commit bac04cc

File tree

7 files changed

+196
-8
lines changed

7 files changed

+196
-8
lines changed

README.md

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Acá comenzamos a ver cómo funciona el mecanismo de routing de Svelte, que est
2222

2323
```
2424
+ src
25-
- routes
25+
+ routes
2626
- +page.svelte <== acá está el menú principal
2727
+ csr
2828
- +page.svelte <== está la página apuntada por la url `/csr`
@@ -80,3 +80,194 @@ Más adelante vamos a ver ejemplos donde quizás necesitemos disparar una búsqu
8080

8181
## Server-side rendering (SSR)
8282

83+
En la carpeta SSR podrás ver que tenemos esta estructura de archivos:
84+
85+
```
86+
+ src
87+
+ routes
88+
+ ssr
89+
- +page.svelte
90+
- +page.server.ts
91+
...
92+
```
93+
94+
### Página svelte
95+
96+
La página Svelte contiene código que vamos a ejecutar en el cliente (el navegador), pero en este caso necesitamos que el procesamiento de la palabra (para obtener la cantidad de letras) lo haga el servidor. La interacción cliente-servidor en web siempre debe comenzar en el servidor, entonces lo más simple en este caso es agregar un botón de Submit que envía la petición al server:
97+
98+
<img src="./images/input_ssr.png" alt="input client side" height="auto" width="30%">
99+
100+
Y para enviar información al server haremos un pedido POST desde un formulario html:
101+
102+
```html
103+
<form method="POST" use:enhance>
104+
<input name="palabra" placeholder="Escribí una palabra" data-testid="palabra" required />
105+
<button type="submit" data-testid="submit">Contar letras</button>
106+
</form>
107+
```
108+
109+
Algunas cuestiones:
110+
111+
- el input tiene un `name="palabra"`, en el Submit lo que hacemos es armar pares clave-valor en base a estos names
112+
- el form indica el tipo de método http que vamos a usar, en este caso POST
113+
- `use:enhance` propio de Svelte intercepta el formulario y hace internamente una llamada al server mediante `fetch`, una función nativa del entorno de los navegadores. Como consecuencia de ese cambio, no vamos a notar la llamada al servidor (lo que produce un típico parpadeo cuando se recarga toda la página)
114+
115+
### Recibiendo la información
116+
117+
Por convención, el archivo `+page.server.ts` se procesa en el servidor:
118+
119+
- recibimos la información de nuestro formulario HTML, básicamente claves/valor donde el valor será siempre un string al viajar por http
120+
- devolvemos un JSON: la información tiene que ser serializable para poder viajar nuevamente del servidor al cliente por http
121+
122+
```ts
123+
export const actions: Actions = {
124+
default: async ({ request }) => {
125+
const formData = await request.formData()
126+
const palabra = formData.get('palabra')?.toString() || ''
127+
const longitud = palabra.length
128+
return { palabra, longitud }
129+
}
130+
}
131+
```
132+
133+
Lo que hace no es gran cosa pero sirve como ejemplo: obtiene la palabra del formulario y devuelve un JSON con la misma palabra y la longitud correspondiente.
134+
135+
![ssr network](./images/ssr_network.gif)
136+
137+
Esa respuesta es recibida por la página Svelte, en el cliente (navegador) para descomponer la información y mostrar un div con el resultado:
138+
139+
```ts
140+
<script lang="ts">
141+
...
142+
export let form: { palabra?: string; longitud?: number } | null
143+
</script>
144+
...
145+
{#if form?.palabra}
146+
<div class="row">
147+
<p data-testid="resultado">La palabra "{form.palabra}" tiene {form.longitud} letras.</p>
148+
</div>
149+
{/if}
150+
```
151+
152+
Cada vez que presionamos el botón estamos yendo al servidor a procesar la palabra:
153+
154+
![ssr several times](./images/ssr_several_times.gif)
155+
156+
## Esquema mixto
157+
158+
Si renombramos nuestro archivo `+page.server.ts` a `+page.ts`, Svelte automáticamente va a trabajar en modo mixto
159+
160+
- la primera vez irá al server a procesar la palabra
161+
- también descargará localmente el código (se transpilará de ts a js)
162+
- las sucesivas veces que presionemos el botón Submit se procesará en el cliente
163+
164+
165+
166+
## Testing
167+
168+
### CSR
169+
170+
El testeo de front es similar a ejemplos anteriores, solo es interesante contar la manera en que probamos que el botón Volver nos lleva al punto raíz de la navegación:
171+
172+
```ts
173+
it('should navigate to the home page when link is clicked', async () => {
174+
render(Counter)
175+
const backLink = screen.getByTestId('back-link') as HTMLAnchorElement
176+
await userEvent.click(backLink)
177+
expect(window.location.pathname).toBe('/')
178+
})
179+
```
180+
181+
- buscamos nuestro anchor (a href) por testid
182+
- y chequeamos que al hacer click sobre el elemento eso cambia variable global `window`
183+
184+
### SSR
185+
186+
Como la estrategia SSR es un poco más compleja necesitamos agregar más tests para cubrir todos los casos. Por ejemplo queremos testear que cuando el server nos devuelve una palabra y su longitud, lo sabemos mostrar en el div:
187+
188+
```ts
189+
it('should show the word length when a word is passed as props', async () => {
190+
render(Counter, {
191+
props: {
192+
form: {
193+
palabra: 'hola',
194+
longitud: 4
195+
}
196+
}
197+
})
198+
const palabra = await screen.getByTestId('palabra') as HTMLInputElement
199+
expect(palabra.value).to.equal('')
200+
expect(screen.getByTestId('resultado').textContent).to.equal('La palabra "hola" tiene 4 letras.')
201+
})
202+
```
203+
204+
Pero también tenemos que chequear la información que pasamos al server cuando escribimos una palabra. Esto hace perder bastante la inocencia de los tests (algo que muchas personas podrían marcar como polémica):
205+
206+
```ts
207+
it('sends the word by doing a submit', async () => {
208+
const mockFetch = vi.fn().mockResolvedValue({
209+
ok: true,
210+
json: async () => ({
211+
type: 'success',
212+
data: { palabra: 'hola', longitud: 4 }
213+
})
214+
})
215+
216+
globalThis.fetch = mockFetch
217+
218+
render(Counter, { props: { form: null } })
219+
220+
const input = screen.getByTestId('palabra')
221+
const submit = screen.getByTestId('submit')
222+
223+
await userEvent.type(input, 'hola')
224+
await userEvent.click(submit)
225+
226+
// Verificar que se llamó fetch
227+
expect(mockFetch).toHaveBeenCalled()
228+
229+
// Verificamos el FormData enviado
230+
const fetchCall = mockFetch.mock.calls[0]
231+
const requestInit = fetchCall[1] as RequestInit
232+
expect(requestInit.body).toBeInstanceOf(URLSearchParams)
233+
const params = new URLSearchParams(requestInit.body as string)
234+
expect(params.get('palabra')).toBe('hola')
235+
})
236+
```
237+
238+
Por qué el test pierde su naturaleza naif de caja negra? Porque sabemos que internamente no se dispara un submit hacia el server sino un fetch, mockeamos en consecuencia esta función y esperamos que dentro del segundo parámetro en la primera llamada haya una clave 'palabra'.
239+
240+
Por último, también debemos testear por separado el comportamiento del server:
241+
242+
```ts
243+
it('should process a word and return its length', async () => {
244+
const mockRequest = {
245+
formData: async () =>
246+
new Map([['palabra', 'svelte']]) as unknown as FormData
247+
}
248+
249+
// Simulamos el objeto RequestEvent mínimamente
250+
const event = { request: mockRequest } as unknown as RequestEvent<RouteParams, '/ssr'>
251+
252+
const result = await actions.default(event)
253+
expect(result).toEqual({
254+
palabra: 'svelte',
255+
longitud: 6
256+
})
257+
})
258+
```
259+
260+
Fíjense que no estamos testeando la interacción cliente/servidor: podríamos cambiar en el archivo `+page.server.ts` el nombre del parámetro a `word`:
261+
262+
```ts
263+
export const actions: Actions = {
264+
default: async ({ request }) => {
265+
const formData = await request.formData()
266+
const palabra = formData.get('word')?.toString() || '' // introducimos un error
267+
const longitud = palabra.length
268+
return { palabra, longitud }
269+
}
270+
}
271+
```
272+
273+
cambiar nuestro test unitario y 1. los tests pasarían correctamente, 2. tendríamos una buena cobertura. Sin embargo la aplicación no funcionaría.

images/input_ssr.png

46.4 KB
Loading

images/ssr_network.gif

293 KB
Loading

images/ssr_several_times.gif

281 KB
Loading

src/routes/csr/countLetters.spec.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import { describe, it, expect, vi, beforeEach } from 'vitest'
1+
import { describe, it, expect } from 'vitest'
22
import { render, screen } from '@testing-library/svelte'
33
import Counter from './+page.svelte'
44
import userEvent from '@testing-library/user-event'
55

66
describe('count letters', () => {
7-
beforeEach(() => {
8-
vi.clearAllMocks()
9-
})
107

118
it('should start with empty string and should show no result', () => {
129
render(Counter)

src/routes/ssr/countLetters.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('count letters', () => {
3737
expect(window.location.pathname).toBe('/')
3838
})
3939

40-
it('envía la palabra correctamente al hacer submit', async () => {
40+
it('sends the word by doing a submit', async () => {
4141
const mockFetch = vi.fn().mockResolvedValue({
4242
ok: true,
4343
json: async () => ({

src/routes/ssr/page.server.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// tests/page.server.test.ts
22
import { describe, it, expect } from 'vitest'
3-
import { actions } from './+page.server'
3+
import { actions } from './+page'
44
import type { RequestEvent } from '@sveltejs/kit'
55
import type { RouteParams } from './$types'
66

77
describe('actions.default', () => {
8-
it('debería devolver palabra y longitud', async () => {
8+
it('should process a word and return its length', async () => {
99
const mockRequest = {
1010
formData: async () =>
1111
new Map([['palabra', 'svelte']]) as unknown as FormData

0 commit comments

Comments
 (0)