diff --git a/Jenkinsfile.ci b/Jenkinsfile.ci index 2de7f6a..51612d5 100644 --- a/Jenkinsfile.ci +++ b/Jenkinsfile.ci @@ -84,6 +84,7 @@ pipeline { mkdir -p "$NPM_CONFIG_CACHE" npm install --no-progress --no-audit --prefer-offline npm run check + npm test npm run build ''' } diff --git a/frontend/.gitignore b/frontend/.gitignore index 4346d24..0152d07 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -6,3 +6,4 @@ dist *.log .svelte-kit vite.config.ts.timestamp-* +TESTS.md diff --git a/frontend/src/App.test.js b/frontend/src/App.test.js new file mode 100644 index 0000000..91aa941 --- /dev/null +++ b/frontend/src/App.test.js @@ -0,0 +1,127 @@ +/* +CI/CD Workshop +Copyright (C) 2025 OpenBokeron + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/svelte'; +import App from './App.svelte'; + +const mockMenu = { + starters: ['Sopa de cocido', 'Ensalada mixta'], + mains: ['Pescado al vapor', 'Pollo asado'], + garnish: ['Patatas fritas'], + desserts: ['Fruta del tiempo', 'Yogur'], + notes: ['Pan y bebida incluidos'], + menu_price: 5.5, + university_deal: { + old_price: 4.5, + current_price: 5.5, + note: 'Gracias al convenio con la Universidad', + }, + alternative: { title: 'Plato alternativo', items: [], price: 5.0 }, + availability: { last_updated: '12:00' }, + espetos_tip: 'Hoy toca sardinas', +}; + +const mockPrices = { + items: [ + { item: 'cafe', price: 1.2, currency: 'EUR', generated_at: '10:00' }, + { item: 'tostada', price: 2.0, currency: 'EUR', generated_at: '10:00' }, + ], +}; + +const mockHealth = { + status: 'ok', + build: 42, + commit: 'abc1234def5678', + author: 'Test Author', + uptime_seconds: 3600, +}; + +const mockBuilds = { + builds: [ + { + number: 42, + status: 'success', + finished_at: Date.now(), + duration_seconds: 60, + commits: [{ commit: 'abc1234', message: 'Fix bug', author: 'Dev' }], + }, + ], +}; + +function createMockFetch(url) { + if (url.includes('/menu')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockMenu) }); + } + if (url.includes('/prices')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPrices) }); + } + if (url.includes('/health')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockHealth) }); + } + if (url.includes('/builds')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockBuilds) }); + } + return Promise.resolve({ ok: false, status: 404 }); +} + +describe('App.svelte', () => { + beforeEach(() => { + globalThis.fetch = vi.fn(createMockFetch); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('renderiza el titulo principal', async () => { + render(App); + + await waitFor(() => { + expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); + }); + }); + + it('renderiza la seccion hero con eyebrow', async () => { + render(App); + + await waitFor(() => { + expect(screen.getByText('Taller CI/CD con Jenkins')).toBeInTheDocument(); + }); + }); + + it('renderiza los botones de accion', async () => { + render(App); + + await waitFor(() => { + expect(screen.getByText('Refrescar menú')).toBeInTheDocument(); + expect(screen.getByText('Recalcular desayunos')).toBeInTheDocument(); + }); + }); + + it('renderiza la tarjeta de Open Bokeron', async () => { + render(App); + + await waitFor(() => { + expect(screen.getByText('Open Bokeron')).toBeInTheDocument(); + expect( + screen.getByText('Somos Open Bokeron, la asociación de software libre de la ETSII.') + ).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/services/api.test.js b/frontend/src/services/api.test.js new file mode 100644 index 0000000..d5c67b0 --- /dev/null +++ b/frontend/src/services/api.test.js @@ -0,0 +1,123 @@ +/* +CI/CD Workshop +Copyright (C) 2025 OpenBokeron + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { getMenu, getPrices, getCiStatus, getBuildHistory } from './api'; + +describe('API services', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + describe('getMenu', () => { + it('devuelve datos del menu cuando la API responde correctamente', async () => { + const mockMenu = { + starters: ['Sopa'], + mains: ['Pescado al vapor'], + desserts: ['Fruta'], + }; + + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockMenu), + }); + + const result = await getMenu(); + + expect(fetch).toHaveBeenCalledWith('/taller/api/menu'); + expect(result).toEqual(mockMenu); + }); + + it('lanza error cuando la API falla', async () => { + fetch.mockResolvedValueOnce({ + ok: false, + status: 500, + }); + + await expect(getMenu()).rejects.toThrow('Respuesta no valida del servidor'); + }); + }); + + describe('getPrices', () => { + it('devuelve array de items cuando la API responde', async () => { + const mockResponse = { + items: [ + { item: 'cafe', price: 1.2 }, + { item: 'tostada', price: 2.0 }, + ], + }; + + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockResponse), + }); + + const result = await getPrices(); + + expect(fetch).toHaveBeenCalledWith('/taller/api/prices'); + expect(result).toEqual(mockResponse.items); + }); + + it('devuelve array vacio si no hay items', async () => { + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({}), + }); + + const result = await getPrices(); + + expect(result).toEqual([]); + }); + }); + + describe('getCiStatus', () => { + it('llama al endpoint /health', async () => { + const mockHealth = { status: 'ok', build: 42 }; + + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockHealth), + }); + + const result = await getCiStatus(); + + expect(fetch).toHaveBeenCalledWith('/taller/api/health'); + expect(result).toEqual(mockHealth); + }); + }); + + describe('getBuildHistory', () => { + it('llama al endpoint /builds', async () => { + const mockBuilds = { builds: [{ number: 1 }, { number: 2 }] }; + + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockBuilds), + }); + + const result = await getBuildHistory(); + + expect(fetch).toHaveBeenCalledWith('/taller/api/builds'); + expect(result).toEqual(mockBuilds); + }); + }); +}); diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js new file mode 100644 index 0000000..e3b07e8 --- /dev/null +++ b/frontend/src/setupTests.js @@ -0,0 +1,19 @@ +/* +CI/CD Workshop +Copyright (C) 2025 OpenBokeron + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import '@testing-library/jest-dom'; diff --git a/frontend/src/utils/text.test.js b/frontend/src/utils/text.test.js new file mode 100644 index 0000000..afe9ee6 --- /dev/null +++ b/frontend/src/utils/text.test.js @@ -0,0 +1,38 @@ +/* +CI/CD Workshop +Copyright (C) 2025 OpenBokeron + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { describe, it, expect } from 'vitest'; +import { prettify } from './text'; + +describe('prettify', () => { + it('convierte snake_case a Title Case', () => { + expect(prettify('hello_world')).toBe('Hello World'); + }); + + it('maneja palabras multiples con underscore', () => { + expect(prettify('cafe_con_leche')).toBe('Cafe Con Leche'); + }); + + it('maneja una sola palabra sin underscore', () => { + expect(prettify('bocadillo')).toBe('Bocadillo'); + }); + + it('maneja cadena vacia', () => { + expect(prettify('')).toBe(''); + }); +}); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index d8aba27..aab9f54 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -14,6 +14,7 @@ export default defineConfig(({ mode }) => { test: { environment: 'jsdom', globals: true, + setupFiles: ['./src/setupTests.js'], }, server: { port: 5173,