Refactor code structure for improved readability and maintainability
This commit is contained in:
803
plans/game-search-architecture.md
Normal file
803
plans/game-search-architecture.md
Normal file
@@ -0,0 +1,803 @@
|
||||
# Arquitectura: Nueva Pantalla de Búsqueda de Juegos
|
||||
|
||||
## Resumen Ejecutivo
|
||||
|
||||
Este documento describe la arquitectura para reemplazar el popup actual ([`GameDialog.tsx`](frontend/src/components/games/GameDialog.tsx)) por una página completa de búsqueda de juegos que permite buscar en múltiples proveedores externos (IGDB, RAWG, TheGamesDB) y seleccionar un resultado para guardar en la base de datos.
|
||||
|
||||
## 1. Arquitectura del Backend
|
||||
|
||||
### 1.1 Modificaciones al Endpoint Existente
|
||||
|
||||
**Endpoint actual:** [`GET /api/metadata/search?q=query&platform=optional`](backend/src/routes/metadata.ts:22)
|
||||
|
||||
**Problema actual:**
|
||||
|
||||
- El endpoint usa [`enrichGame()`](backend/src/services/metadataService.ts:34) que solo devuelve el primer resultado encontrado
|
||||
- La respuesta es un array con un solo elemento o vacío
|
||||
|
||||
**Solución propuesta:**
|
||||
Crear una nueva función en [`metadataService.ts`](backend/src/services/metadataService.ts) que busque en TODOS los proveedores y devuelva múltiples resultados.
|
||||
|
||||
### 1.2 Nueva Función: `searchGames()`
|
||||
|
||||
**Ubicación:** [`backend/src/services/metadataService.ts`](backend/src/services/metadataService.ts)
|
||||
|
||||
```typescript
|
||||
export async function searchGames(opts: {
|
||||
title: string;
|
||||
platform?: string;
|
||||
year?: number;
|
||||
}): Promise<EnrichedGame[]> {
|
||||
// Buscar en IGDB, RAWG y TheGamesDB simultáneamente
|
||||
// Devolver todos los resultados encontrados
|
||||
// Aplicar deduplicación basada en nombre + plataforma + año
|
||||
}
|
||||
```
|
||||
|
||||
**Lógica de búsqueda:**
|
||||
|
||||
1. Ejecutar búsquedas en paralelo en los tres proveedores
|
||||
2. Normalizar todos los resultados usando la función [`normalize()`](backend/src/services/metadataService.ts:12) existente
|
||||
3. Aplicar deduplicación para evitar duplicados entre proveedores
|
||||
4. Ordenar resultados por relevancia (coincidencia exacta primero)
|
||||
5. Limitar resultados (ej: 20 resultados máximos por proveedor, 50 total)
|
||||
|
||||
**Deduplicación:**
|
||||
|
||||
- Agrupar por nombre normalizado (lowercase, sin caracteres especiales)
|
||||
- Si hay duplicados, priorizar: IGDB > RAWG > TheGamesDB
|
||||
- Mantener `externalIds` de todas las fuentes para cada resultado
|
||||
|
||||
### 1.3 Modificación del Endpoint
|
||||
|
||||
**Ubicación:** [`backend/src/routes/metadata.ts`](backend/src/routes/metadata.ts)
|
||||
|
||||
**Cambios:**
|
||||
|
||||
1. Actualizar el esquema de validación para incluir `year` como parámetro opcional
|
||||
2. Llamar a `searchGames()` en lugar de `enrichGame()`
|
||||
3. Devolver el array de resultados directamente
|
||||
|
||||
```typescript
|
||||
const searchMetadataSchema = z.object({
|
||||
q: z.string().min(1, 'El parámetro de búsqueda es requerido'),
|
||||
platform: z.string().optional(),
|
||||
year: z.coerce.number().int().min(1900).max(2100).optional(),
|
||||
});
|
||||
|
||||
app.get('/metadata/search', async (request, reply) => {
|
||||
const validated = searchMetadataSchema.parse(request.query);
|
||||
const results = await metadataService.searchGames({
|
||||
title: validated.q,
|
||||
platform: validated.platform,
|
||||
year: validated.year,
|
||||
});
|
||||
return reply.code(200).send(results);
|
||||
});
|
||||
```
|
||||
|
||||
### 1.4 Estructura de Respuesta de la API
|
||||
|
||||
```typescript
|
||||
// GET /api/metadata/search?q=mario&year=1990&platform=NES
|
||||
|
||||
[
|
||||
{
|
||||
source: 'igdb',
|
||||
externalIds: {
|
||||
igdb: 1234,
|
||||
rawg: 5678,
|
||||
thegamesdb: 9012,
|
||||
},
|
||||
name: 'Super Mario Bros.',
|
||||
title: 'Super Mario Bros.',
|
||||
slug: 'super-mario-bros',
|
||||
releaseDate: '1985-09-13T00:00:00.000Z',
|
||||
genres: ['Platform'],
|
||||
coverUrl: 'https://example.com/cover.jpg',
|
||||
platforms: [
|
||||
{
|
||||
id: 18,
|
||||
name: 'Nintendo Entertainment System (NES)',
|
||||
abbreviation: 'NES',
|
||||
slug: 'nes',
|
||||
},
|
||||
],
|
||||
},
|
||||
// ... más resultados
|
||||
];
|
||||
```
|
||||
|
||||
### 1.5 Nuevo Endpoint para Guardar Resultado
|
||||
|
||||
**Endpoint:** `POST /api/games/from-metadata`
|
||||
|
||||
**Propósito:** Crear un juego a partir de un resultado de búsqueda de metadatos.
|
||||
|
||||
**Body:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
"metadata": {
|
||||
"source": "igdb",
|
||||
"externalIds": { "igdb": 1234 },
|
||||
"name": "Super Mario Bros.",
|
||||
"slug": "super-mario-bros",
|
||||
"releaseDate": "1985-09-13T00:00:00.000Z",
|
||||
"genres": ["Platform"],
|
||||
"coverUrl": "https://example.com/cover.jpg"
|
||||
},
|
||||
"overrides": {
|
||||
"platform": "NES",
|
||||
"year": 1985,
|
||||
"description": "Descripción personalizada..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Lógica:**
|
||||
|
||||
1. Mapear metadatos a estructura de [`Game`](backend/prisma/schema.prisma:12)
|
||||
2. Aplicar overrides si se proporcionan
|
||||
3. Guardar en base de datos usando [`GamesController.createGame()`](backend/src/controllers/gamesController.ts:73)
|
||||
4. Devolver el juego creado
|
||||
|
||||
### 1.6 Diagrama de Flujo del Backend
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Cliente envía búsqueda] --> B[Validar parámetros]
|
||||
B --> C[searchGames]
|
||||
C --> D[Paralelo: IGDB]
|
||||
C --> E[Paralelo: RAWG]
|
||||
C --> F[Paralelo: TheGamesDB]
|
||||
D --> G[Normalizar resultados]
|
||||
E --> G
|
||||
F --> G
|
||||
G --> H[Deduplicar]
|
||||
H --> I[Ordenar por relevancia]
|
||||
I --> J[Limitar resultados]
|
||||
J --> K[Devolver array]
|
||||
K --> L[Cliente muestra resultados]
|
||||
L --> M[Usuario selecciona resultado]
|
||||
M --> N[POST /api/games/from-metadata]
|
||||
N --> O[GamesController.createGame]
|
||||
O --> P[Guardar en BD]
|
||||
P --> Q[Devolver juego creado]
|
||||
```
|
||||
|
||||
## 2. Arquitectura del Frontend
|
||||
|
||||
### 2.1 Nueva Ruta
|
||||
|
||||
**Ruta:** `/games/add` o `/games/search`
|
||||
|
||||
**Archivo:** [`frontend/src/app/games/add/page.tsx`](frontend/src/app/games/add/page.tsx)
|
||||
|
||||
**Razón:** `/games/add` es más semántico y sigue el patrón RESTful para crear recursos.
|
||||
|
||||
### 2.2 Componentes a Crear
|
||||
|
||||
#### 2.2.1 SearchForm Component
|
||||
|
||||
**Ubicación:** [`frontend/src/components/games/SearchForm.tsx`](frontend/src/components/games/SearchForm.tsx)
|
||||
|
||||
**Responsabilidades:**
|
||||
|
||||
- Formulario de búsqueda con campos: título (obligatorio), año (opcional), plataforma (opcional)
|
||||
- Validación de formulario
|
||||
- Disparar evento de búsqueda
|
||||
|
||||
**Componentes shadcn/ui a usar:**
|
||||
|
||||
- [`Input`](frontend/src/components/ui/input.tsx) para título y año
|
||||
- [`Select`](frontend/src/components/ui/select.tsx) para plataforma
|
||||
- [`Button`](frontend/src/components/ui/button.tsx) para submit
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface SearchFormProps {
|
||||
onSearch: (params: SearchParams) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2.2 SearchResults Component
|
||||
|
||||
**Ubicación:** [`frontend/src/components/games/SearchResults.tsx`](frontend/src/components/games/SearchResults.tsx)
|
||||
|
||||
**Responsabilidades:**
|
||||
|
||||
- Mostrar lista de resultados de búsqueda
|
||||
- Permitir selección de un resultado
|
||||
- Mostrar estado de carga
|
||||
|
||||
**Componentes shadcn/ui a usar:**
|
||||
|
||||
- [`Card`](frontend/src/components/ui/card.tsx) para cada resultado
|
||||
- [`Button`](frontend/src/components/ui/button.tsx) para seleccionar
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface SearchResultsProps {
|
||||
results: MetadataGame[];
|
||||
onSelect: (result: MetadataGame) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2.3 GamePreviewDialog Component
|
||||
|
||||
**Ubicación:** [`frontend/src/components/games/GamePreviewDialog.tsx`](frontend/src/components/games/GamePreviewDialog.tsx)
|
||||
|
||||
**Responsabilidades:**
|
||||
|
||||
- Mostrar detalles completos del resultado seleccionado
|
||||
- Permitir editar campos antes de guardar
|
||||
- Confirmar guardado
|
||||
|
||||
**Componentes shadcn/ui a usar:**
|
||||
|
||||
- [`Dialog`](frontend/src/components/ui/dialog.tsx) para el modal
|
||||
- [`Input`](frontend/src/components/ui/input.tsx) para campos editables
|
||||
- [`Textarea`](frontend/src/components/ui/textarea.tsx) para descripción
|
||||
- [`Button`](frontend/src/components/ui/button.tsx) para acciones
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface GamePreviewDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
metadata: MetadataGame | null;
|
||||
onSave: (metadata: MetadataGame, overrides: GameOverrides) => void;
|
||||
isSaving: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Flujo de Usuario
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Usuario accede a /games/add] --> B[SearchForm visible]
|
||||
B --> C[Usuario ingresa título]
|
||||
C --> D[Usuario hace clic en Buscar]
|
||||
D --> E[LLAMADA API: GET /api/metadata/search]
|
||||
E --> F[SearchResults muestra resultados]
|
||||
F --> G[Usuario ve lista de juegos]
|
||||
G --> H[Usuario hace clic en un resultado]
|
||||
H --> I[GamePreviewDialog se abre]
|
||||
I --> J[Usuario puede editar campos]
|
||||
J --> K[Usuario hace clic en Guardar]
|
||||
K --> L[LLAMADA API: POST /api/games/from-metadata]
|
||||
L --> M{Éxito?}
|
||||
M -->|Sí| N[Redirigir a /games o /games/id]
|
||||
M -->|No| O[Mostrar error en dialog]
|
||||
O --> J
|
||||
```
|
||||
|
||||
### 2.4 Estructura de la Página
|
||||
|
||||
```typescript
|
||||
// frontend/src/app/games/add/page.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { SearchForm } from '@/components/games/SearchForm';
|
||||
import { SearchResults } from '@/components/games/SearchResults';
|
||||
import { GamePreviewDialog } from '@/components/games/GamePreviewDialog';
|
||||
import { metadataApi, gamesApi } from '@/lib/api';
|
||||
|
||||
export default function AddGamePage() {
|
||||
const router = useRouter();
|
||||
const [searchParams, setSearchParams] = useState<SearchParams | null>(null);
|
||||
const [results, setResults] = useState<MetadataGame[]>([]);
|
||||
const [selectedResult, setSelectedResult] = useState<MetadataGame | null>(null);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleSearch = async (params: SearchParams) => {
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const data = await metadataApi.search(params);
|
||||
setResults(data);
|
||||
setSearchParams(params);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (result: MetadataGame) => {
|
||||
setSelectedResult(result);
|
||||
};
|
||||
|
||||
const handleSave = async (metadata: MetadataGame, overrides: GameOverrides) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const game = await gamesApi.createFromMetadata(metadata, overrides);
|
||||
router.push(`/games/${game.id}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<h1 className="text-3xl font-bold mb-8">Añadir Juego</h1>
|
||||
|
||||
<SearchForm onSearch={handleSearch} isLoading={isSearching} />
|
||||
|
||||
{results.length > 0 && (
|
||||
<SearchResults
|
||||
results={results}
|
||||
onSelect={handleSelect}
|
||||
isLoading={isSearching}
|
||||
/>
|
||||
)}
|
||||
|
||||
<GamePreviewDialog
|
||||
open={!!selectedResult}
|
||||
onOpenChange={(open) => !open && setSelectedResult(null)}
|
||||
metadata={selectedResult}
|
||||
onSave={handleSave}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 Actualización del Cliente API
|
||||
|
||||
**Ubicación:** [`frontend/src/lib/api.ts`](frontend/src/lib/api.ts)
|
||||
|
||||
**Nuevas funciones:**
|
||||
|
||||
```typescript
|
||||
// Búsqueda de metadatos
|
||||
export async function searchMetadata(params: {
|
||||
q: string;
|
||||
platform?: string;
|
||||
year?: number;
|
||||
}): Promise<MetadataGame[]> {
|
||||
const queryParams = new URLSearchParams({ q: params.q });
|
||||
if (params.platform) queryParams.append('platform', params.platform);
|
||||
if (params.year) queryParams.append('year', String(params.year));
|
||||
|
||||
const res = await fetch(`${API_BASE}/metadata/search?${queryParams}`);
|
||||
if (!res.ok) throw new Error('Error searching metadata');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Crear juego desde metadatos
|
||||
export async function createGameFromMetadata(
|
||||
metadata: MetadataGame,
|
||||
overrides?: GameOverrides
|
||||
): Promise<Game> {
|
||||
const res = await fetch(`${API_BASE}/games/from-metadata`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ metadata, overrides }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Error creating game from metadata');
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 Integración con Página de Juegos
|
||||
|
||||
**Ubicación:** [`frontend/src/app/games/page.tsx`](frontend/src/app/games/page.tsx)
|
||||
|
||||
**Cambio:**
|
||||
|
||||
- Reemplazar el botón "Nuevo Juego" que abre [`GameDialog`](frontend/src/components/games/GameDialog.tsx) por un enlace a `/games/add`
|
||||
- Mantener [`GameDialog`](frontend/src/components/games/GameDialog.tsx) solo para edición de juegos existentes
|
||||
|
||||
```typescript
|
||||
// Antes
|
||||
<Button onClick={handleCreate}>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
Nuevo Juego
|
||||
</Button>
|
||||
|
||||
// Después
|
||||
<Link href="/games/add">
|
||||
<Button>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
Añadir Juego
|
||||
</Button>
|
||||
</Link>
|
||||
```
|
||||
|
||||
## 3. Estrategia TDD (Test Driven Development)
|
||||
|
||||
### 3.1 Orden de Implementación
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Tests Backend - searchGames] --> B[Implementar searchGames]
|
||||
B --> C[Tests Backend - Endpoint /metadata/search]
|
||||
C --> D[Implementar Endpoint]
|
||||
D --> E[Tests Backend - POST /api/games/from-metadata]
|
||||
E --> F[Implementar Endpoint from-metadata]
|
||||
F --> G[Tests Frontend - Componentes Unitarios]
|
||||
G --> H[Implementar Componentes]
|
||||
H --> I[Tests E2E - Flujo Completo]
|
||||
I --> J[Validar Integración]
|
||||
```
|
||||
|
||||
### 3.2 Tests del Backend
|
||||
|
||||
#### 3.2.1 Tests de `searchGames()`
|
||||
|
||||
**Archivo:** [`backend/tests/services/metadataService.search.spec.ts`](backend/tests/services/metadataService.search.spec.ts)
|
||||
|
||||
**Casos de prueba:**
|
||||
|
||||
1. ✅ Debe devolver resultados de IGDB cuando hay coincidencias
|
||||
2. ✅ Debe devolver resultados de RAWG cuando IGDB falla
|
||||
3. ✅ Debe devolver resultados de TheGamesDB cuando IGDB y RAWG fallan
|
||||
4. ✅ Debe deduplicar resultados entre proveedores
|
||||
5. ✅ Debe priorizar IGDB sobre RAWG y TheGamesDB
|
||||
6. ✅ Debe filtrar por plataforma cuando se proporciona
|
||||
7. ✅ Debe filtrar por año cuando se proporciona
|
||||
8. ✅ Debe devolver array vacío cuando no hay resultados
|
||||
9. ✅ Debe manejar errores de API externas gracefully
|
||||
10. ✅ Debe limitar número de resultados
|
||||
|
||||
**Ejemplo de test:**
|
||||
|
||||
```typescript
|
||||
it('debe deduplicar resultados entre proveedores', async () => {
|
||||
const mockIgdbResult = {
|
||||
name: 'Super Mario Bros.',
|
||||
source: 'igdb',
|
||||
id: 1234,
|
||||
// ... otros campos
|
||||
};
|
||||
|
||||
const mockRawgResult = {
|
||||
name: 'Super Mario Bros.',
|
||||
source: 'rawg',
|
||||
id: 5678,
|
||||
// ... otros campos
|
||||
};
|
||||
|
||||
vi.spyOn(igdb, 'searchGames').mockResolvedValue([mockIgdbResult]);
|
||||
vi.spyOn(rawg, 'searchGames').mockResolvedValue([mockRawgResult]);
|
||||
|
||||
const results = await metadataService.searchGames({ title: 'mario' });
|
||||
|
||||
// Solo debería haber un resultado con ambos IDs externos
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].externalIds.igdb).toBe(1234);
|
||||
expect(results[0].externalIds.rawg).toBe(5678);
|
||||
});
|
||||
```
|
||||
|
||||
#### 3.2.2 Tests del Endpoint `/api/metadata/search`
|
||||
|
||||
**Archivo:** [`backend/tests/routes/metadata.search.spec.ts`](backend/tests/routes/metadata.search.spec.ts)
|
||||
|
||||
**Casos de prueba:**
|
||||
|
||||
1. ✅ Debe devolver 200 con array de resultados
|
||||
2. ✅ Debe devolver 400 si falta parámetro `q`
|
||||
3. ✅ Debe devolver 400 si `year` no es válido
|
||||
4. ✅ Debe pasar parámetros a `searchGames()`
|
||||
5. ✅ Debe manejar errores del servicio
|
||||
|
||||
#### 3.2.3 Tests del Endpoint `POST /api/games/from-metadata`
|
||||
|
||||
**Archivo:** [`backend/tests/routes/games.from-metadata.spec.ts`](backend/tests/routes/games.from-metadata.spec.ts)
|
||||
|
||||
**Casos de prueba:**
|
||||
|
||||
1. ✅ Debe crear juego con metadatos de IGDB
|
||||
2. ✅ Debe crear juego con metadatos de RAWG
|
||||
3. ✅ Debe crear juego con metadatos de TheGamesDB
|
||||
4. ✅ Debe aplicar overrides cuando se proporcionan
|
||||
5. ✅ Debe guardar `externalIds` correctamente
|
||||
6. ✅ Debe devolver 400 si metadata es inválida
|
||||
7. ✅ Debe devolver 201 con juego creado
|
||||
|
||||
### 3.3 Tests del Frontend
|
||||
|
||||
#### 3.3.1 Tests Unitarios de Componentes
|
||||
|
||||
**Archivo:** [`frontend/src/components/games/__tests__/SearchForm.test.tsx`](frontend/src/components/games/__tests__/SearchForm.test.tsx)
|
||||
|
||||
**Casos de prueba:**
|
||||
|
||||
1. ✅ Debe mostrar campos de formulario
|
||||
2. ✅ Debe validar que título es obligatorio
|
||||
3. ✅ Debe llamar `onSearch` con parámetros correctos
|
||||
4. ✅ Debe deshabilitar botón cuando `isLoading` es true
|
||||
|
||||
**Archivo:** [`frontend/src/components/games/__tests__/SearchResults.test.tsx`](frontend/src/components/games/__tests__/SearchResults.test.tsx)
|
||||
|
||||
**Casos de prueba:**
|
||||
|
||||
1. ✅ Debe mostrar lista de resultados
|
||||
2. ✅ Debe mostrar mensaje cuando no hay resultados
|
||||
3. ✅ Debe llamar `onSelect` cuando se hace clic en resultado
|
||||
4. ✅ Debe mostrar estado de carga
|
||||
|
||||
**Archivo:** [`frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx`](frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx)
|
||||
|
||||
**Casos de prueba:**
|
||||
|
||||
1. ✅ Debe mostrar metadatos del juego
|
||||
2. ✅ Debe permitir editar campos
|
||||
3. ✅ Debe llamar `onSave` con metadatos y overrides
|
||||
4. ✅ Debe cerrar cuando se cancela
|
||||
|
||||
#### 3.3.2 Tests E2E
|
||||
|
||||
**Archivo:** [`tests/e2e/game-search-flow.spec.ts`](tests/e2e/game-search-flow.spec.ts)
|
||||
|
||||
**Casos de prueba:**
|
||||
|
||||
1. ✅ Flujo completo: búsqueda → selección → guardado
|
||||
2. ✅ Búsqueda sin resultados
|
||||
3. ✅ Cancelar selección
|
||||
4. ✅ Editar campos antes de guardar
|
||||
5. ✅ Validación de errores
|
||||
|
||||
**Ejemplo de test E2E:**
|
||||
|
||||
```typescript
|
||||
test('flujo completo de búsqueda y guardado', async ({ page }) => {
|
||||
await page.goto('/games/add');
|
||||
|
||||
// Ingresar búsqueda
|
||||
await page.fill('input[name="title"]', 'Super Mario');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Esperar resultados
|
||||
await page.waitForSelector('[data-testid="search-results"]');
|
||||
const results = await page.locator('[data-testid="game-result"]').count();
|
||||
expect(results).toBeGreaterThan(0);
|
||||
|
||||
// Seleccionar primer resultado
|
||||
await page.click('[data-testid="game-result"]:first-child');
|
||||
|
||||
// Esperar dialog de preview
|
||||
await page.waitForSelector('[data-testid="preview-dialog"]');
|
||||
|
||||
// Guardar
|
||||
await page.click('[data-testid="save-button"]');
|
||||
|
||||
// Verificar redirección
|
||||
await page.waitForURL(/\/games\/[a-z0-9]+$/);
|
||||
});
|
||||
```
|
||||
|
||||
### 3.4 Orden de Implementación Recomendado
|
||||
|
||||
1. **Sprint 1: Backend Core**
|
||||
- Tests de `searchGames()`
|
||||
- Implementar `searchGames()`
|
||||
- Tests del endpoint `/api/metadata/search`
|
||||
- Actualizar endpoint `/api/metadata/search`
|
||||
|
||||
2. **Sprint 2: Backend Save**
|
||||
- Tests del endpoint `POST /api/games/from-metadata`
|
||||
- Implementar endpoint `POST /api/games/from-metadata`
|
||||
|
||||
3. **Sprint 3: Frontend Components**
|
||||
- Tests unitarios de `SearchForm`
|
||||
- Implementar `SearchForm`
|
||||
- Tests unitarios de `SearchResults`
|
||||
- Implementar `SearchResults`
|
||||
- Tests unitarios de `GamePreviewDialog`
|
||||
- Implementar `GamePreviewDialog`
|
||||
|
||||
4. **Sprint 4: Frontend Page & Integration**
|
||||
- Implementar página `/games/add`
|
||||
- Actualizar cliente API
|
||||
- Integrar con página `/games`
|
||||
|
||||
5. **Sprint 5: E2E & Polish**
|
||||
- Tests E2E del flujo completo
|
||||
- Corregir bugs encontrados
|
||||
- Mejorar UX (loading states, error messages)
|
||||
|
||||
## 4. Integración con Código Existente
|
||||
|
||||
### 4.1 Código a Reutilizar
|
||||
|
||||
#### Backend
|
||||
|
||||
| Componente | Archivo | Uso |
|
||||
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------- |
|
||||
| [`normalize()`](backend/src/services/metadataService.ts:12) | [`metadataService.ts`](backend/src/services/metadataService.ts) | Normalizar resultados de búsqueda |
|
||||
| [`igdb.searchGames()`](backend/src/services/igdbClient.ts:73) | [`igdbClient.ts`](backend/src/services/igdbClient.ts) | Buscar en IGDB |
|
||||
| [`rawg.searchGames()`](backend/src/services/rawgClient.ts:13) | [`rawgClient.ts`](backend/src/services/rawgClient.ts) | Buscar en RAWG |
|
||||
| [`thegamesdb.searchGames()`](backend/src/services/thegamesdbClient.ts:18) | [`thegamesdbClient.ts`](backend/src/services/thegamesdbClient.ts) | Buscar en TheGamesDB |
|
||||
| [`GamesController.createGame()`](backend/src/controllers/gamesController.ts:73) | [`gamesController.ts`](backend/src/controllers/gamesController.ts) | Crear juego en BD |
|
||||
| [`Game`](backend/prisma/schema.prisma:12) schema | [`schema.prisma`](backend/prisma/schema.prisma) | Modelo de datos |
|
||||
|
||||
#### Frontend
|
||||
|
||||
| Componente | Archivo | Uso |
|
||||
| ------------------------------------------------- | ----------------------------------------------------- | ---------------------- |
|
||||
| [`Button`](frontend/src/components/ui/button.tsx) | [`button.tsx`](frontend/src/components/ui/button.tsx) | Botones de acción |
|
||||
| [`Input`](frontend/src/components/ui/input.tsx) | [`input.tsx`](frontend/src/components/ui/input.tsx) | Campos de formulario |
|
||||
| [`Select`](frontend/src/components/ui/select.tsx) | [`select.tsx`](frontend/src/components/ui/select.tsx) | Selector de plataforma |
|
||||
| [`Card`](frontend/src/components/ui/card.tsx) | [`card.tsx`](frontend/src/components/ui/card.tsx) | Tarjetas de resultados |
|
||||
| [`Dialog`](frontend/src/components/ui/dialog.tsx) | [`dialog.tsx`](frontend/src/components/ui/dialog.tsx) | Dialog de preview |
|
||||
| [`MetadataGame`](frontend/src/lib/api.ts:39) type | [`api.ts`](frontend/src/lib/api.ts) | Tipo de metadatos |
|
||||
| [`Game`](frontend/src/lib/api.ts:57) type | [`api.ts`](frontend/src/lib/api.ts) | Tipo de juego |
|
||||
|
||||
### 4.2 Código a Modificar
|
||||
|
||||
#### Backend
|
||||
|
||||
| Archivo | Cambio |
|
||||
| --------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| [`metadataService.ts`](backend/src/services/metadataService.ts) | Agregar función `searchGames()` |
|
||||
| [`metadata.ts`](backend/src/routes/metadata.ts) | Actualizar endpoint `/api/metadata/search` |
|
||||
| [`games.ts`](backend/src/routes/games.ts) | Agregar endpoint `POST /api/games/from-metadata` |
|
||||
| [`types/index.ts`](backend/src/types/index.ts) | Agregar tipos para búsqueda múltiple |
|
||||
|
||||
#### Frontend
|
||||
|
||||
| Archivo | Cambio |
|
||||
| ---------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| [`api.ts`](frontend/src/lib/api.ts) | Agregar funciones `searchMetadata()` y `createGameFromMetadata()` |
|
||||
| [`games/page.tsx`](frontend/src/app/games/page.tsx) | Cambiar botón "Nuevo Juego" a enlace `/games/add` |
|
||||
| [`GameDialog.tsx`](frontend/src/components/games/GameDialog.tsx) | Mantener solo para edición (opcional) |
|
||||
|
||||
### 4.3 Código Nuevo a Crear
|
||||
|
||||
#### Backend
|
||||
|
||||
| Archivo | Propósito |
|
||||
| ---------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
||||
| [`backend/tests/services/metadataService.search.spec.ts`](backend/tests/services/metadataService.search.spec.ts) | Tests de `searchGames()` |
|
||||
| [`backend/tests/routes/games.from-metadata.spec.ts`](backend/tests/routes/games.from-metadata.spec.ts) | Tests del endpoint from-metadata |
|
||||
|
||||
#### Frontend
|
||||
|
||||
| Archivo | Propósito |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- |
|
||||
| [`frontend/src/app/games/add/page.tsx`](frontend/src/app/games/add/page.tsx) | Página de búsqueda de juegos |
|
||||
| [`frontend/src/components/games/SearchForm.tsx`](frontend/src/components/games/SearchForm.tsx) | Formulario de búsqueda |
|
||||
| [`frontend/src/components/games/SearchResults.tsx`](frontend/src/components/games/SearchResults.tsx) | Lista de resultados |
|
||||
| [`frontend/src/components/games/GamePreviewDialog.tsx`](frontend/src/components/games/GamePreviewDialog.tsx) | Dialog de preview y edición |
|
||||
| [`frontend/src/components/games/__tests__/SearchForm.test.tsx`](frontend/src/components/games/__tests__/SearchForm.test.tsx) | Tests de SearchForm |
|
||||
| [`frontend/src/components/games/__tests__/SearchResults.test.tsx`](frontend/src/components/games/__tests__/SearchResults.test.tsx) | Tests de SearchResults |
|
||||
| [`frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx`](frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx) | Tests de GamePreviewDialog |
|
||||
| [`tests/e2e/game-search-flow.spec.ts`](tests/e2e/game-search-flow.spec.ts) | Tests E2E del flujo |
|
||||
|
||||
## 5. Diagrama de Arquitectura Completa
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Frontend
|
||||
A[Página /games/add] --> B[SearchForm]
|
||||
A --> C[SearchResults]
|
||||
A --> D[GamePreviewDialog]
|
||||
B --> E[api.ts - searchMetadata]
|
||||
C --> F[api.ts - createGameFromMetadata]
|
||||
D --> F
|
||||
end
|
||||
|
||||
subgraph Backend
|
||||
E --> G[GET /api/metadata/search]
|
||||
F --> H[POST /api/games/from-metadata]
|
||||
G --> I[metadataService.searchGames]
|
||||
I --> J[igdbClient.searchGames]
|
||||
I --> K[rawgClient.searchGames]
|
||||
I --> L[thegamesdbClient.searchGames]
|
||||
H --> M[GamesController.createGame]
|
||||
M --> N[(Prisma - Game)]
|
||||
end
|
||||
|
||||
subgraph External APIs
|
||||
J --> O[IGDB API]
|
||||
K --> P[RAWG API]
|
||||
L --> Q[TheGamesDB API]
|
||||
end
|
||||
|
||||
style Frontend fill:#e1f5fe
|
||||
style Backend fill:#f3e5f5
|
||||
style External APIs fill:#fff3e0
|
||||
```
|
||||
|
||||
## 6. Consideraciones Adicionales
|
||||
|
||||
### 6.1 Manejo de Errores
|
||||
|
||||
**Backend:**
|
||||
|
||||
- Capturar errores de APIs externas sin fallar completamente
|
||||
- Devolver resultados parciales si algún proveedor falla
|
||||
- Logging de errores para debugging
|
||||
|
||||
**Frontend:**
|
||||
|
||||
- Mostrar mensajes de error claros al usuario
|
||||
- Permitir reintentar búsqueda
|
||||
- Mostrar estado de carga durante búsquedas
|
||||
|
||||
### 6.2 Performance
|
||||
|
||||
**Backend:**
|
||||
|
||||
- Usar `Promise.all()` para búsquedas paralelas
|
||||
- Implementar caching de resultados (opcional)
|
||||
- Limitar número de resultados por proveedor
|
||||
|
||||
**Frontend:**
|
||||
|
||||
- Implementar debounce en búsqueda
|
||||
- Lazy loading de imágenes de portada
|
||||
- Paginación de resultados si hay muchos
|
||||
|
||||
### 6.3 Seguridad
|
||||
|
||||
**Backend:**
|
||||
|
||||
- Validar todos los parámetros de entrada
|
||||
- Sanitizar datos antes de guardar en BD
|
||||
- Rate limiting en endpoints de búsqueda
|
||||
|
||||
**Frontend:**
|
||||
|
||||
- Sanitizar HTML antes de mostrar
|
||||
- Validar en cliente y servidor
|
||||
|
||||
### 6.4 Accesibilidad
|
||||
|
||||
**Frontend:**
|
||||
|
||||
- Labels en todos los campos de formulario
|
||||
- Focus management en dialogs
|
||||
- Keyboard navigation
|
||||
- ARIA labels donde sea necesario
|
||||
|
||||
### 6.5 UX Considerations
|
||||
|
||||
- Mostrar loading states claros
|
||||
- Feedback inmediato al usuario
|
||||
- Permitir cancelar búsqueda en progreso
|
||||
- Mostrar fuente de cada resultado (IGDB, RAWG, etc.)
|
||||
- Permitir ver más detalles antes de seleccionar
|
||||
- Confirmación antes de guardar
|
||||
|
||||
## 7. Resumen de Cambios
|
||||
|
||||
### Backend
|
||||
|
||||
| Archivo | Acción |
|
||||
| ----------------------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| [`metadataService.ts`](backend/src/services/metadataService.ts) | Agregar `searchGames()` |
|
||||
| [`metadata.ts`](backend/src/routes/metadata.ts) | Modificar endpoint `/api/metadata/search` |
|
||||
| [`games.ts`](backend/src/routes/games.ts) | Agregar endpoint `POST /api/games/from-metadata` |
|
||||
| [`types/index.ts`](backend/src/types/index.ts) | Agregar tipos nuevos |
|
||||
| [`metadataService.search.spec.ts`](backend/tests/services/metadataService.search.spec.ts) | **NUEVO** |
|
||||
| [`games.from-metadata.spec.ts`](backend/tests/routes/games.from-metadata.spec.ts) | **NUEVO** |
|
||||
|
||||
### Frontend
|
||||
|
||||
| Archivo | Acción |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------- |
|
||||
| [`api.ts`](frontend/src/lib/api.ts) | Agregar `searchMetadata()` y `createGameFromMetadata()` |
|
||||
| [`games/page.tsx`](frontend/src/app/games/page.tsx) | Cambiar botón a enlace |
|
||||
| [`games/add/page.tsx`](frontend/src/app/games/add/page.tsx) | **NUEVO** |
|
||||
| [`SearchForm.tsx`](frontend/src/components/games/SearchForm.tsx) | **NUEVO** |
|
||||
| [`SearchResults.tsx`](frontend/src/components/games/SearchResults.tsx) | **NUEVO** |
|
||||
| [`GamePreviewDialog.tsx`](frontend/src/components/games/GamePreviewDialog.tsx) | **NUEVO** |
|
||||
| Tests unitarios | **NUEVOS** |
|
||||
| [`game-search-flow.spec.ts`](tests/e2e/game-search-flow.spec.ts) | **NUEVO** |
|
||||
|
||||
---
|
||||
|
||||
**Documento creado:** 2025-03-21
|
||||
**Autor:** Architect Mode
|
||||
**Estado:** Diseño completo listo para implementación
|
||||
Reference in New Issue
Block a user