Centro de Práctica
Módulos API: practice (19 endpoints) · Páginas web: PracticeCenterPage, PracticeQuizPlayerPage, PracticeHistoryPage
Specs Playwright: apps/e2e/tests/student/practice.spec.ts — 📋 pendientes de crear
Mapa de endpoints cubiertos
Section titled “Mapa de endpoints cubiertos”| Método | Endpoint | Rol mínimo | Historia |
|---|---|---|---|
GET | /practice/suggestions | Autenticado | US-PRAC-001 |
POST | /practice/suggestions/:id/dismiss | Autenticado | US-PRAC-002 |
GET | /practice/modules | Autenticado | US-PRAC-003 |
POST | /practice/sessions | Autenticado | US-PRAC-003 |
PATCH | /practice/sessions/:id | Autenticado | US-PRAC-004 |
GET | /practice/sessions | Autenticado | US-PRAC-006 |
GET | /practice/stats | Autenticado | US-PRAC-005 |
GET | /practice/quizzes/:id/player | Autenticado | US-PRAC-004 |
POST | /practice/quizzes | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-007 |
GET | /practice/quizzes/:id | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-007 |
PATCH | /practice/quizzes/:id | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-007 |
DELETE | /practice/quizzes/:id | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-007 |
POST | /practice/quizzes/:id/questions | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-007 |
PATCH | /practice/quizzes/:id/questions/:qId | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-007 |
DELETE | /practice/quizzes/:id/questions/:qId | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-007 |
PATCH | /practice/quizzes/:id/questions/reorder | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-007 |
POST | /practice/quizzes/:id/import-from-generation | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-008 |
GET | /practice/items-by-module/:moduleId | CONTENT_ADMIN, SUPER_ADMIN | US-PRAC-007 |
User Stories
Section titled “User Stories”US-PRAC-001: Ver sugerencias de práctica adaptativas
Section titled “US-PRAC-001: Ver sugerencias de práctica adaptativas”Como estudiante autenticado Quiero ver las sugerencias que el motor adaptativo ha identificado para mí Para saber qué temas debo reforzar sin tener que decidirlo manualmente
Criterios: AC-PRAC-001 · Casos: TC-PRAC-001, TC-PRAC-002 · Prioridad: Alta · ✅ Implementado
US-PRAC-002: Descartar una sugerencia de práctica
Section titled “US-PRAC-002: Descartar una sugerencia de práctica”Como estudiante autenticado Quiero descartar sugerencias de práctica que no me interesan o ya domino Para mantener la lista limpia y relevante sin ser interrumpido por alertas irrelevantes
Criterios: AC-PRAC-002 · Casos: TC-PRAC-003, TC-PRAC-004 · Prioridad: Media · ✅ Implementado
US-PRAC-003: Iniciar una sesión de práctica libre
Section titled “US-PRAC-003: Iniciar una sesión de práctica libre”Como estudiante autenticado Quiero elegir un módulo y comenzar una sesión de práctica libre Para repasar contenido a mi propio ritmo, sin restricciones de calificación
Criterios: AC-PRAC-003 · Casos: TC-PRAC-005, TC-PRAC-006 · Prioridad: Alta · ✅ Implementado
US-PRAC-004: Completar y enviar una sesión de práctica
Section titled “US-PRAC-004: Completar y enviar una sesión de práctica”Como estudiante en una sesión de práctica activa Quiero poder completar el quiz o actividad y enviar mis resultados Para que el sistema registre mi progreso y recalcule las sugerencias adaptativas
Criterios: AC-PRAC-004 · Casos: TC-PRAC-007, TC-PRAC-008 · Prioridad: Alta · ✅ Implementado
US-PRAC-005: Ver mis estadísticas de práctica acumuladas
Section titled “US-PRAC-005: Ver mis estadísticas de práctica acumuladas”Como estudiante autenticado Quiero ver un resumen de mis estadísticas de práctica: sesiones semanales, tiempo total, precisión media y racha Para entender mi nivel de constancia y mi evolución en el tiempo
Criterios: AC-PRAC-005 · Casos: TC-PRAC-009 · Prioridad: Media · ✅ Implementado
US-PRAC-006: Ver el historial de sesiones de práctica
Section titled “US-PRAC-006: Ver el historial de sesiones de práctica”Como estudiante autenticado Quiero ver una lista paginada de todas mis sesiones de práctica anteriores Para revisar en qué módulos he practicado, cuándo y con qué resultados
Criterios: AC-PRAC-006 · Casos: TC-PRAC-010, TC-PRAC-011 · Prioridad: Media · ✅ Implementado
US-PRAC-007: Crear y gestionar un quiz de práctica (creator)
Section titled “US-PRAC-007: Crear y gestionar un quiz de práctica (creator)”Como CONTENT_ADMIN o SUPER_ADMIN Quiero crear quizzes de práctica con preguntas de tipo opción múltiple, verdadero/falso y respuesta abierta Para dotar a los módulos de material de práctica estructurado para los estudiantes
Criterios: AC-PRAC-007 · Casos: TC-PRAC-012, TC-PRAC-013 · Prioridad: Alta · ✅ Implementado
US-PRAC-008: Importar preguntas generadas por IA a un quiz de práctica
Section titled “US-PRAC-008: Importar preguntas generadas por IA a un quiz de práctica”Como CONTENT_ADMIN o SUPER_ADMIN Quiero importar preguntas generadas por el motor de IA directamente a un quiz de práctica existente Para agilizar la creación de contenido de práctica sin escribir las preguntas manualmente
Criterios: AC-PRAC-008 · Casos: TC-PRAC-014 · Prioridad: Media · ✅ Implementado
Acceptance Criteria
Section titled “Acceptance Criteria”AC-PRAC-001: Ver sugerencias de práctica adaptativas
Section titled “AC-PRAC-001: Ver sugerencias de práctica adaptativas”US: US-PRAC-001
AC-PRAC-001.1 — Lista de sugerencias activas
Dado que soy un estudiante con historial de actividad (quizzes fallidos o bajo score)Cuando accedo a "/practice"Entonces veo hasta 5 sugerencias activas ordenadas por prioridad ascendenteY cada sugerencia muestra el módulo, el path al que pertenece, y la razón (quiz_failed, low_score, time_lapsed, no_attempt)Y las sugerencias con dismissedAt !== null no aparecen en la listaAC-PRAC-001.2 — Estado vacío cuando no hay sugerencias
Dado que soy un estudiante sin sugerencias activas pendientesCuando accedo a la sección "Recomendadas para ti"Entonces veo el mensaje de estado vacío indicando que estoy al díaY no se muestra ninguna tarjeta de sugerenciaAC-PRAC-001.3 — Acceso restringido a autenticados
Dado que soy un visitante no autenticadoCuando llamo a GET /practice/suggestions sin token JWTEntonces la API responde con status 401AC-PRAC-002: Descartar una sugerencia
Section titled “AC-PRAC-002: Descartar una sugerencia”US: US-PRAC-002
AC-PRAC-002.1 — Descarte propio exitoso
Dado que tengo una sugerencia activa con ID "sug-abc"Cuando hago clic en "Descartar" en esa sugerenciaEntonces la API responde con status 200 o 201Y la sugerencia desaparece de la lista visible (dismissedAt se rellena)Y el motor la puede regenerar si la condición persiste en la próxima evaluaciónAC-PRAC-002.2 — No se puede descartar sugerencia de otro usuario
Dado que la sugerencia "sug-xyz" pertenece al usuario BCuando el usuario A llama a POST /practice/suggestions/sug-xyz/dismissEntonces la API responde con status 403AC-PRAC-002.3 — Sugerencia inexistente retorna 404
Dado que el ID "sug-nonexistent" no existe en la BDCuando llamo a POST /practice/suggestions/sug-nonexistent/dismissEntonces la API responde con status 404AC-PRAC-003: Iniciar una sesión de práctica libre
Section titled “AC-PRAC-003: Iniciar una sesión de práctica libre”US: US-PRAC-003
AC-PRAC-003.1 — Módulos de práctica disponibles solo para módulos con progreso
Dado que soy estudiante con progreso en los módulos M1 y M2Cuando llamo a GET /practice/modulesEntonces recibo únicamente M1 y M2 con sus practiceItems publicadosY cada módulo incluye lastPracticedAt, practiceCount y lastScorePercentAC-PRAC-003.2 — Búsqueda y filtrado de módulos
Dado que tengo progreso en varios módulosCuando llamo a GET /practice/modules?search=python&filter=with_itemsEntonces solo se devuelven módulos cuyo título contiene "python" y que tienen al menos un practiceItem publicadoAC-PRAC-003.3 — Creación de sesión libre exitosa
Dado que selecciono el módulo M1 y el ítem de práctica P1Cuando llamo a POST /practice/sessions con { moduleId, practiceItemId, mode: "FREE" }Entonces la API responde con status 201 y { sessionId: "<uuid>" }Y se crea un registro PracticeSession en BD con studentId = mi IDAC-PRAC-003.4 — Límite diario de 5 sesiones por ítem
Dado que ya he iniciado 5 sesiones de práctica para el ítem P1 hoyCuando intento crear una 6ª sesión para P1Entonces la API responde con status 400Y el mensaje de error indica que se alcanzó el límite diarioAC-PRAC-003.5 — Módulo inexistente retorna 404
Dado que el moduleId enviado no existe en BDCuando llamo a POST /practice/sessionsEntonces la API responde con status 404AC-PRAC-004: Completar y enviar una sesión de práctica
Section titled “AC-PRAC-004: Completar y enviar una sesión de práctica”US: US-PRAC-004
AC-PRAC-004.1 — Completar sesión con scorePercent
Dado que tengo una sesión activa con sessionId "ses-123"Cuando llamo a PATCH /practice/sessions/ses-123 con { scorePercent: 85, completedInSec: 240 }Entonces la API responde con la sesión actualizada con completedAt rellenoY se dispara una notificación de tipo PRACTICE_SESSION_COMPLETED al estudianteY el motor adaptativo re-evalúa sugerencias en background (fire-and-forget)AC-PRAC-004.2 — Sugerencia vinculada se descarta automáticamente al completar
Dado que la sesión estaba vinculada a un practiceItem con una sugerencia activa en el mismo móduloCuando completo la sesiónEntonces la sugerencia activa de ese módulo queda marcada con dismissedAtAC-PRAC-004.3 — No se puede completar sesión de otro usuario
Dado que la sesión "ses-123" pertenece al usuario BCuando el usuario A llama a PATCH /practice/sessions/ses-123Entonces la API responde con status 403AC-PRAC-004.4 — Player del quiz: cálculo de score
Dado que abro un quiz de práctica con preguntas SINGLE_CHOICE y TRUE_FALSECuando respondo todas las preguntas y envíoEntonces la página calcula el scorePercent localmente antes de mostrar resultadosY la pregunta de tipo OPEN_ENDED requiere autoevaluación (correcto / incorrecto) para contribuir al scoreY al finalizar se llama a PATCH /practice/sessions/:id con el scorePercent calculadoAC-PRAC-005: Ver estadísticas de práctica acumuladas
Section titled “AC-PRAC-005: Ver estadísticas de práctica acumuladas”US: US-PRAC-005
AC-PRAC-005.1 — Métricas correctas en el strip de estadísticas
Dado que soy un estudiante con historial de sesionesCuando accedo a GET /practice/statsEntonces recibo sessionsThisWeek, sessionsPrevWeek, totalTimeSec, avgScorePercent, practiceStreak y heatmap (14 días)Y avgScorePercent solo promedia sesiones con completedAt y scorePercent no nulosY practiceStreak cuenta días consecutivos hacia atrás desde hoy con al menos 1 sesiónAC-PRAC-005.2 — Racha se muestra en la UI como heatmap de 14 días
Dado que accedo a la página de prácticaCuando el componente StreakPracticeCard cargaEntonces veo 14 cuadros de colores representando la actividad diariaY los días sin sesión aparecen en gris, con 1 sesión en amarillo, con 2+ en verdeAC-PRAC-005.3 — Estudiante sin sesiones recibe métricas en cero
Dado que soy un estudiante sin ninguna sesión de práctica previaCuando llamo a GET /practice/statsEntonces recibo { sessionsThisWeek: 0, totalTimeSec: 0, avgScorePercent: 0, practiceStreak: 0 }AC-PRAC-006: Ver historial de sesiones de práctica
Section titled “AC-PRAC-006: Ver historial de sesiones de práctica”US: US-PRAC-006
AC-PRAC-006.1 — Lista paginada de sesiones, ordenada por más reciente
Dado que tengo 45 sesiones registradasCuando llamo a GET /practice/sessions?page=1&limit=20Entonces recibo { items: [...20 sesiones], total: 45, page: 1, limit: 20 }Y las sesiones están ordenadas por createdAt descendenteY cada sesión incluye moduleTitle, mode, scorePercent, completedInSec y createdAtAC-PRAC-006.2 — Estado vacío en historial sin sesiones
Dado que soy un estudiante sin sesiones previasCuando accedo a "/practice/history"Entonces veo el estado vacío con CTA para ir a practicarY no se muestra ninguna fila en la listaAC-PRAC-006.3 — Solo sesiones propias del estudiante
Dado que la BD tiene sesiones de múltiples usuariosCuando llamo a GET /practice/sessionsEntonces solo recibo mis propias sesiones (filtrado por studentId = mi ID)AC-PRAC-007: Crear y gestionar un quiz de práctica (creator)
Section titled “AC-PRAC-007: Crear y gestionar un quiz de práctica (creator)”US: US-PRAC-007
AC-PRAC-007.1 — Creación de quiz y vínculo automático al banco de práctica
Dado que soy CONTENT_ADMIN y el módulo M1 existeCuando llamo a POST /practice/quizzes con { moduleId, title, description }Entonces se crea un PracticeQuiz con status DRAFTY se crea automáticamente un PracticeItem de tipo PRACTICE_QUIZ vinculado al módulo M1Y la respuesta incluye el quiz con sus preguntas (vacías inicialmente)AC-PRAC-007.2 — Añadir preguntas a un quiz
Dado que tengo un PracticeQuiz con ID "quiz-123"Cuando llamo a POST /practice/quizzes/quiz-123/questions con { type: "SINGLE_CHOICE", question: "¿Qué es X?", options: [...], correctAnswer: "opt-1" }Entonces se crea la pregunta con order incrementado automáticamenteAC-PRAC-007.3 — Reordenar preguntas
Dado que el quiz "quiz-123" tiene las preguntas [q1, q2, q3]Cuando llamo a PATCH /practice/quizzes/quiz-123/questions/reorder con { questionIds: ["q3","q1","q2"] }Entonces el order de las preguntas se actualiza en BDY la API responde con status 200AC-PRAC-007.4 — Eliminar pregunta renormaliza el orden
Dado que el quiz tiene preguntas con order [0, 1, 2]Cuando elimino la pregunta con order 1 (DELETE /practice/quizzes/:id/questions/:qId)Entonces las preguntas restantes quedan con order [0, 1] (sin huecos)AC-PRAC-007.5 — Acceso restringido a roles creator
Dado que soy un usuario con rol FREE, STARTER, PRO o ELITECuando llamo a POST /practice/quizzesEntonces la API responde con status 403AC-PRAC-008: Importar preguntas generadas por IA
Section titled “AC-PRAC-008: Importar preguntas generadas por IA”US: US-PRAC-008
AC-PRAC-008.1 — Importación exitosa de preguntas de IA
Dado que tengo un PracticeQuiz "quiz-123" y una tarea de IA con ID "task-456" con preguntas aceptadasCuando llamo a POST /practice/quizzes/quiz-123/import-from-generation con { taskId, acceptedQuestions: [...] }Entonces se crean las preguntas en BD con aiGenerated = trueY la AITask se marca con reviewStatus = "ACCEPTED"Y la respuesta incluye { importedCount: N }AC-PRAC-008.2 — Tipos de pregunta se normalizan correctamente
Dado que las preguntas generadas tienen tipo MULTIPLE_CHOICECuando se importan al quiz de prácticaEntonces se almacenan como SINGLE_CHOICE (normalización de tipo)Y las preguntas TRUE_FALSE y preguntas abiertas mantienen sus tipos correspondientesAC-PRAC-008.3 — Quiz inexistente retorna 404
Dado que el quizId enviado no existeCuando llamo a POST /practice/quizzes/nonexistent/import-from-generationEntonces la API responde con status 404Test Cases
Section titled “Test Cases”TC-PRAC-001: Sugerencias activas se muestran correctamente
Section titled “TC-PRAC-001: Sugerencias activas se muestran correctamente”AC: AC-PRAC-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/practice.spec.ts
Precondiciones: fixtures.pro tiene sesiones con score bajo en al menos un módulo
const res = await apiClient.get('/practice/suggestions', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(res.status).toBe(200)expect(Array.isArray(res.data)).toBe(true)expect(res.data.length).toBeLessThanOrEqual(5)
// Verificar estructura de cada sugerenciaconst suggestion = res.data[0]expect(suggestion).toHaveProperty('id')expect(suggestion).toHaveProperty('moduleTitle')expect(suggestion).toHaveProperty('pathTitle')expect(suggestion).toHaveProperty('reason')expect(suggestion).toHaveProperty('priority')expect(suggestion.dismissedAt).toBeNull()
// Sugerencias están ordenadas por prioridad ascendenteconst priorities = res.data.map((s: { priority: number }) => s.priority)const sorted = [...priorities].sort((a: number, b: number) => a - b)expect(priorities).toEqual(sorted)TC-PRAC-002: Sin token retorna 401 en sugerencias
Section titled “TC-PRAC-002: Sin token retorna 401 en sugerencias”AC: AC-PRAC-001.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
const res = await apiClient.get('/practice/suggestions')expect(res.status).toBe(401)TC-PRAC-003: Descartar sugerencia propia
Section titled “TC-PRAC-003: Descartar sugerencia propia”AC: AC-PRAC-002.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/practice.spec.ts
Precondiciones: fixtures.pro tiene al menos una sugerencia activa
// 1. Obtener sugerencias activasconst listRes = await apiClient.get('/practice/suggestions', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(listRes.data.length).toBeGreaterThan(0)const suggestionId = listRes.data[0].id
// 2. Descartar la primera sugerenciaconst dismissRes = await apiClient.post( `/practice/suggestions/${suggestionId}/dismiss`, {}, { headers: { Authorization: `Bearer ${fixtures.pro.token}` } })expect(dismissRes.status).toBeLessThan(300)
// 3. Verificar que ya no aparece en la lista activaconst listAfterRes = await apiClient.get('/practice/suggestions', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})const ids = listAfterRes.data.map((s: { id: string }) => s.id)expect(ids).not.toContain(suggestionId)TC-PRAC-004: No se puede descartar sugerencia de otro usuario
Section titled “TC-PRAC-004: No se puede descartar sugerencia de otro usuario”AC: AC-PRAC-002.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
// fixtures.pro tiene la sugerencia, fixtures.starter intenta descartarlaconst listRes = await apiClient.get('/practice/suggestions', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})const proSuggestionId = listRes.data[0]?.idif (!proSuggestionId) return // skip si no hay sugerencias
const res = await apiClient.post( `/practice/suggestions/${proSuggestionId}/dismiss`, {}, { headers: { Authorization: `Bearer ${fixtures.starter.token}` } })expect(res.status).toBe(403)TC-PRAC-005: Iniciar sesión de práctica libre
Section titled “TC-PRAC-005: Iniciar sesión de práctica libre”AC: AC-PRAC-003.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/practice.spec.ts
Precondiciones: fixtures.pro tiene progreso en al menos un módulo con practiceItems publicados
// Obtener módulos de práctica disponiblesconst modulesRes = await apiClient.get('/practice/modules', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(modulesRes.status).toBe(200)const moduleWithItems = modulesRes.data.items.find( (m: { practiceItems: unknown[] }) => m.practiceItems.length > 0)if (!moduleWithItems) return // skip si no hay módulos con items
const practiceItem = moduleWithItems.practiceItems[0]
// Crear sesiónconst sessionRes = await apiClient.post( '/practice/sessions', { moduleId: moduleWithItems.moduleId, practiceItemId: practiceItem.id, mode: 'FREE', triggerReason: 'manual', }, { headers: { Authorization: `Bearer ${fixtures.pro.token}` } })expect(sessionRes.status).toBe(201)expect(sessionRes.data).toHaveProperty('sessionId')expect(typeof sessionRes.data.sessionId).toBe('string')TC-PRAC-006: Límite diario de sesiones por ítem
Section titled “TC-PRAC-006: Límite diario de sesiones por ítem”AC: AC-PRAC-003.4 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
// Este test requiere fixtures con 5 sesiones pre-existentes para el mismo ítem hoy.// Crear una sexta sesión debe fallar.const res = await apiClient.post( '/practice/sessions', { moduleId: fixtures.pro.moduleWithDailyLimit.moduleId, practiceItemId: fixtures.pro.moduleWithDailyLimit.practiceItemId, mode: 'FREE', }, { headers: { Authorization: `Bearer ${fixtures.pro.token}` } })expect(res.status).toBe(400)expect(res.data.message).toMatch(/límite/)TC-PRAC-007: Completar sesión con score registrado
Section titled “TC-PRAC-007: Completar sesión con score registrado”AC: AC-PRAC-004.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/practice.spec.ts
// 1. Crear sesiónconst sessionRes = await apiClient.post( '/practice/sessions', { moduleId: fixtures.pro.moduleId, mode: 'FREE' }, { headers: { Authorization: `Bearer ${fixtures.pro.token}` } })const sessionId = sessionRes.data.sessionId
// 2. Completar la sesiónconst completeRes = await apiClient.patch( `/practice/sessions/${sessionId}`, { scorePercent: 75, completedInSec: 180, completedAt: new Date().toISOString(), }, { headers: { Authorization: `Bearer ${fixtures.pro.token}` } })expect(completeRes.status).toBe(200)expect(completeRes.data.scorePercent).toBe(75)expect(completeRes.data.completedAt).not.toBeNull()expect(completeRes.data.completedInSec).toBe(180)TC-PRAC-008: No se puede completar sesión ajena
Section titled “TC-PRAC-008: No se puede completar sesión ajena”AC: AC-PRAC-004.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
// Crear sesión con fixtures.proconst sessionRes = await apiClient.post( '/practice/sessions', { moduleId: fixtures.pro.moduleId, mode: 'FREE' }, { headers: { Authorization: `Bearer ${fixtures.pro.token}` } })const sessionId = sessionRes.data.sessionId
// Intentar completar con fixtures.starterconst res = await apiClient.patch( `/practice/sessions/${sessionId}`, { scorePercent: 50 }, { headers: { Authorization: `Bearer ${fixtures.starter.token}` } })expect(res.status).toBe(403)TC-PRAC-009: Estadísticas de práctica — estructura y valores correctos
Section titled “TC-PRAC-009: Estadísticas de práctica — estructura y valores correctos”AC: AC-PRAC-005.1, AC-PRAC-005.3 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
// Estadísticas con historial (fixtures.pro)const resWithHistory = await apiClient.get('/practice/stats', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(resWithHistory.status).toBe(200)const stats = resWithHistory.dataexpect(stats).toHaveProperty('sessionsThisWeek')expect(stats).toHaveProperty('sessionsPrevWeek')expect(stats).toHaveProperty('totalTimeSec')expect(stats).toHaveProperty('avgScorePercent')expect(stats).toHaveProperty('practiceStreak')expect(stats).toHaveProperty('heatmap')expect(Array.isArray(stats.heatmap)).toBe(true)expect(stats.heatmap).toHaveLength(14)expect(stats.heatmap[0]).toHaveProperty('date')expect(stats.heatmap[0]).toHaveProperty('count')
// Estadísticas sin historial (fixtures.free)const resEmpty = await apiClient.get('/practice/stats', { headers: { Authorization: `Bearer ${fixtures.free.token}` },})expect(resEmpty.data.sessionsThisWeek).toBe(0)expect(resEmpty.data.totalTimeSec).toBe(0)expect(resEmpty.data.avgScorePercent).toBe(0)expect(resEmpty.data.practiceStreak).toBe(0)TC-PRAC-010: Historial de sesiones paginado
Section titled “TC-PRAC-010: Historial de sesiones paginado”AC: AC-PRAC-006.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/practice.spec.ts
const res = await apiClient.get('/practice/sessions?page=1&limit=20', { headers: { Authorization: `Bearer ${fixtures.pro.token}` },})expect(res.status).toBe(200)expect(res.data).toHaveProperty('items')expect(res.data).toHaveProperty('total')expect(res.data).toHaveProperty('page')expect(res.data).toHaveProperty('limit')expect(Array.isArray(res.data.items)).toBe(true)expect(res.data.items.length).toBeLessThanOrEqual(20)
// Verificar que las sesiones están ordenadas por fecha descendenteif (res.data.items.length > 1) { const dates = res.data.items.map( (s: { createdAt: string }) => new Date(s.createdAt).getTime() ) for (let i = 0; i < dates.length - 1; i++) { expect(dates[i]).toBeGreaterThanOrEqual(dates[i + 1]) }}
// Verificar que solo devuelve sesiones propiasconst studentIds = res.data.items.map((s: { studentId: string }) => s.studentId)const uniqueStudentIds = [...new Set(studentIds)]expect(uniqueStudentIds).toHaveLength(1)expect(uniqueStudentIds[0]).toBe(fixtures.pro.userId)TC-PRAC-011: Historial vacío en PracticeHistoryPage
Section titled “TC-PRAC-011: Historial vacío en PracticeHistoryPage”AC: AC-PRAC-006.2 · Prioridad: Baja · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/practice.spec.ts
// data-testid a confirmar en PracticeHistoryPage:// 'practice-history-empty', 'go-to-practice-btn'await loginAs(page, fixtures.free)await page.goto('/practice/history')await expect(page.getByTestId('practice-history-empty')).toBeVisible()await page.getByTestId('go-to-practice-btn').click()await expect(page).toHaveURL('/practice')TC-PRAC-012: Crear quiz de práctica con restricción de rol
Section titled “TC-PRAC-012: Crear quiz de práctica con restricción de rol”AC: AC-PRAC-007.1, AC-PRAC-007.5 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
// Creación exitosa como CONTENT_ADMIN// (requiere token de content_admin en fixtures)const createRes = await apiClient.post( '/practice/quizzes', { moduleId: fixtures.contentAdmin.testModuleId, title: 'Quiz de prueba QA', description: 'Creado por test automatizado', }, { headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } })expect(createRes.status).toBe(201)expect(createRes.data).toHaveProperty('id')expect(createRes.data.status).toBe('DRAFT')expect(createRes.data.questions).toHaveLength(0)
// Acceso denegado a roles de estudianteconst forbiddenRes = await apiClient.post( '/practice/quizzes', { moduleId: fixtures.pro.moduleId, title: 'No permitido' }, { headers: { Authorization: `Bearer ${fixtures.pro.token}` } })expect(forbiddenRes.status).toBe(403)TC-PRAC-013: Añadir preguntas y reordenar en quiz de práctica
Section titled “TC-PRAC-013: Añadir preguntas y reordenar en quiz de práctica”AC: AC-PRAC-007.2, AC-PRAC-007.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
const quizId = fixtures.contentAdmin.testQuizId
// Añadir pregunta de opción múltipleconst q1Res = await apiClient.post( `/practice/quizzes/${quizId}/questions`, { type: 'SINGLE_CHOICE', question: '¿Cuál es la capital de Francia?', options: [ { id: 'a', text: 'Londres' }, { id: 'b', text: 'París' }, { id: 'c', text: 'Berlín' }, ], correctAnswer: 'b', explanation: 'París es la capital de Francia.', }, { headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } })expect(q1Res.status).toBe(201)const q1Id = q1Res.data.id
// Añadir segunda pregunta V/Fconst q2Res = await apiClient.post( `/practice/quizzes/${quizId}/questions`, { type: 'TRUE_FALSE', question: 'El sol es una estrella', correctAnswer: 'true', }, { headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } })const q2Id = q2Res.data.id
// Reordenar: q2 primero, q1 segundoconst reorderRes = await apiClient.patch( `/practice/quizzes/${quizId}/questions/reorder`, { questionIds: [q2Id, q1Id] }, { headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } })expect(reorderRes.status).toBe(200)expect(reorderRes.data).toEqual({ ok: true })TC-PRAC-014: Importar preguntas desde generación IA
Section titled “TC-PRAC-014: Importar preguntas desde generación IA”AC: AC-PRAC-008.1, AC-PRAC-008.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
const quizId = fixtures.contentAdmin.testQuizIdconst aiTaskId = fixtures.contentAdmin.testAiTaskId // AITask con status PENDING
const importRes = await apiClient.post( `/practice/quizzes/${quizId}/import-from-generation`, { taskId: aiTaskId, acceptedQuestions: [ { questionText: '¿Qué es el aprendizaje automático?', type: 'MULTIPLE_CHOICE', options: [ { id: 'o1', text: 'Un tipo de base de datos', isCorrect: false }, { id: 'o2', text: 'Un subconjunto de la IA que aprende de datos', isCorrect: true }, ], correctAnswer: null, explanation: 'El ML es la capacidad de aprender sin ser programado explícitamente.', }, { questionText: 'Python es un lenguaje interpretado', type: 'TRUE_FALSE', options: [], correctAnswer: 'true', explanation: null, }, ], }, { headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } })expect(importRes.status).toBe(201)expect(importRes.data.importedCount).toBe(2)
// Verificar que la pregunta MULTIPLE_CHOICE fue convertida a SINGLE_CHOICEconst quizRes = await apiClient.get( `/practice/quizzes/${quizId}`, { headers: { Authorization: `Bearer ${fixtures.contentAdmin.token}` } })const importedQ = quizRes.data.questions.find( (q: { aiGenerated: boolean }) => q.aiGenerated === true)expect(importedQ).toBeDefined()expect(importedQ.type).toBe('SINGLE_CHOICE')