En este ejemplo podemos estudiar
Este proyecto utiliza variables de entorno para configurar parámetros que pueden variar según el entorno (desarrollo, producción, test). SvelteKit las carga automáticamente según el modo de ejecución.
.env → variables base comunes a todos los entornos. .env.development → se carga al ejecutar el proyecto en modo desarrollo..env.production → se carga al generar el build o ejecutar en producción..env.local → variables locales de tu máquina, no versionadas, que sobrescriben las demás si existen.PUBLIC_ son accesibles tanto en el cliente como en el servidor.PUBLIC_ solo están disponibles en el servidor..env al repositorio ( en este ejemplo se excluyen de gitignore por cuestiones didácticas ).development, production, test) aunque los valores sean iguales en algunos casos..env.local para configuraciones personales que no deben compartirse..env.[mode].local > .env.[mode] > .env.local > .env
.env.[mode].local sobrescribe .env.[mode]..env.local sobrescribe .env.Es necesario ejecutar el script a fin de que las variables de entorno estén disponibles en el proyecto:
yarn run check
En la página inicial tenemos en la variante inicial
<script lang='ts'>
let paisBusqueda = $state('')
let buscarHabilitado = $derived(paisBusqueda.trim() !== '')
const handleKeydown = ...
</script>
...
<input
data-testid='paisBusqueda'
onkeydown={handleKeydown}
bind:value={paisBusqueda}
placeholder='Ingrese un valor para buscar países'
/>
<button data-testid='buscar' onclick={buscar} disabled={!buscarHabilitado}>Buscar</button>
Como vamos a pedir información a una fuente externa, no podemos saber a priori cuánto va a tardar, entonces tenemos que hacer una llamada asincrónica. Definimos entonces un componente aparte, que llamamos service y que tendrá como responsabilidades
En la página escribimos
const buscar = async () => {
paises = await paisService.buscarPais(paisBusqueda)
}
y el service se define además como una clase pero exponemos una instancia, que termina siendo un Singleton:
class PaisService {
async buscarPais(paisBusqueda: string): Promise<Pais[]> {
const response = await axios.get(`${PUBLIC_API_BASE_URL}/${PUBLIC_API_VERSION}/name/${paisBusqueda}`)
return response.data.map(toPais)
}
...
}
export const paisService = new PaisService()
Estamos utilizando la biblioteca axios que sabe hacer pedidos por http y devolver el resultado con el formato de una response. Para el siguiente ejemplo veremos cómo hacer el manejo de errores.
La función toPais pueden verla, se encarga de construir un objeto de dominio en base a la información que nos llega. Eso es útil porque un País sabe
y podría tomar más responsabilidades. Además, en este caso no tenemos control sobre la API a la que le llamamos, por lo que es una opción más que rentable.
Una vez que tenemos la lista de países, los mostramos en una grilla:
{#each paises as pais}
<button
class='pais'
data-testid={`pais-${pais.codigo}`}
onclick={() => goto(`/pais/${pais.codigo}`)}
>
<Bandera bandera={pais.bandera}/>
<div class='nombre_pais'>{pais.nombre}</div>
</button>
{/each}
El lector podrá pensar, ¿por qué un button y no un div? Esto tiene que ver con recomendaciones de usabilidad que Svelte aplica, por lo que si usamos un div te va a aparecer un molesto warning:
Para más información podés ver este artículo.
La url a la que vamos se activa con el botón:
... onclick={() => goto(`/pais/${pais.codigo}`)}
// `goto` es una función provista por una biblioteca que viene con Svelte
Para Argentina, esto equivale a http://localhost:5173/pais/ARG. ¿Quién responde a este pedido del cliente?
Como cuenta esta página, el mecanismo de routing sigue una convención, donde toda ruta se ubica por defecto dentro de nuestro proyecto en la carpeta src/routes. Por eso la página principal es +page.svelte pero además tenemos esta estructura
- pais # carpeta con un nombre fijo
- [code] # carpeta encerrada entre corchetes, marca una ruta parametrizable
- +page.svelte # la página Svelte que va a ser invocada cuando llamemos a esta ruta
Ok, sabemos cómo llegamos a la página Svelte, pero todavía falta tomar una decisión: el pedido del cliente
¿Qué nos conviene más? Cada opción tiene ventajas y desventajas, por el momento vamos a optar por la opción 1 ó client-side rendering, entonces escribimos un archivo de nombre page.ts, donde vamos a procesar el parámetro con el código del país y haremos una llamada asincrónica a la API rest countries para obtener el objeto de dominio país: esto no es estrictamente necesario pero lo hacemos para mostrar didácticamente lo que en la mayoría de las aplicaciones sí tiene sentido.
import { paisService } from '$lib/paisService'
export async function load({ params }) {
const pais = await paisService.datosDePais(params.code)
return { pais }
}
La forma de obtener los parámetros es mediante la variable global params, donde debemos respetar el mismo nombre que el que definimos en la carpeta.
Luego la operación se completa recibiendo en la página una $props pais:
let { data } = $props()
const { pais } = data
El lector puede profundizar viendo la implementación del ejemplo.
Tenemos que instalar localmente Playwright para lo cual escribimos
yarn playwright install-deps
yarn playwright install
Ahora sí podemos ejecutar los tests end to end, que lo podemos hacer
Para eso escribimos yarn run test:e2e, y eso nos abrirá un menú interactivo donde nos carga cada uno de los tests y allí seleccionamos el test que queremos ejecutar:
Más allá de las cuestiones técnicas de los tests end to end, podemos hacer un breve resumen de lo que aplicamos en este ejemplo:
Playwright tiene una configuración donde define cuáles son los archivos de testeo end-to-end, así como la carpeta, porque recordemos que los tests e2e no reemplazan sino que complementan los tests unitarios:
import { defineConfig } from '@playwright/test'
export default defineConfig({
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'e2e'
})
Ahora sí vamos a ver el test, no demasiado diferente a lo que veníamos haciendo:
test('flujo principal: buscamos un país y al hacer click nos dirige a la página con la información de dicho país', async ({
page
}) => {
await page.goto('/')
await expect(page.locator('h2')).toBeVisible()
await page.getByTestId('paisBusqueda').fill('ARGENTINA')
await page.getByTestId('buscar').click()
const unPais = await page.getByTestId('pais-0')
await expect(unPais).toBeVisible()
...
})
Sí podrán notar que todas las operaciones son asincrónicas, y requieren envolverse dentro de un await. Eso permite que esperemos un tiempo prudencial (5 segundos) hasta que el elemento que buscamos aparezca en el DOM. Seguimos prefiriendo la búsqueda por data-testid, o por role más que por clases css o texto.
Otra cosa buena es que podemos abstraer flujos, como ir a la página de un país y volver al menú principal. Lo que sí debemos tener en cuenta es que necesitamos pasar la
pagecomo parámetro porque es el objeto donde se conserva el estado actual de nuestros tests e2e.
Por suerte Playwright, al igual que otras tecnologías como Cypress, Selenium o TestCafé, ofrecen buenos mecanismos de análisis de errores:
Vamos a agregar un último feature, queremos permitir la búsqueda automática, a medida que escribimos. Pero la API tiene una limitación de 10 requests por minuto. Entonces no podemos capturar el evento onKeyDown ni onKeyUp. Lo que necesitamos es tomarnos un tiempo para enviar la consulta, concepto que se llama debouncing y que podemos ver con un ejemplo visual.
Lo que hacemos es demorar la llamada a nuestra función mediante un setTimeout. Pero ojo, no alcanza con hacer eso siempre, porque fíjense lo que pasa si escribimos
finalmente estaríamos llamando igualmente a la API 3 veces. En lugar de esta estrategia, lo que vamos a hacer es:
Ahora sí, solamente estamos llamando una vez a la API cuando el usuario demoró lo suficiente (1 segundo al menos).
Veamos la implementación:
let timeout: ReturnType<typeof setTimeout>
const debounce = (callback: Function, wait: number) => {
return (...args: any[]) => {
clearTimeout(timeout)
timeout = setTimeout(() => { callback(...args) }, wait)
}
}
const handleKeyUpBuscar = async () => {
if (busquedaAutomatica && buscarHabilitado) {
debounce(buscar, 1000)()
}
}
y la función handleKeyUpBuscar se asocia al evento onKeyup del input de búsqueda:
<input
data-testid='paisBusqueda'
onkeydown={handleKeyDownBuscar}
onkeyup={handleKeyUpBuscar}
bind:value={paisBusqueda}
placeholder='Ingrese un valor para buscar países'
/>
Agregamos también un renderizado condicional del botón de Buscar y un checkbox bindeado a una propiedad que nos indica si la búsqueda es automática o no. Dejamos al lector que profundice mirando el ejemplo.
Como consecuencia de esta técnica, podemos abrir el navegador en la solapa Network y ver cómo se dispara una sola vez la búsqueda: